個人開発の壁紙アプリを更新する前夜、ドイツ語の端末で保存完了のメッセージを出したところ、数字の入るはずの場所が空のまま表示されました。
原因は strings.xml の一行です。基準の言語では %1$s と %2$d が入っているのに、ドイツ語側は %1$s しか書かれていませんでした。クラッシュには至らず、画面に出た文言が少しおかしいだけ。だからこそ、レビューをすり抜けていたのです。
そのとき私は、エージェントに「全言語の strings.xml を点検して」と頼んでみました。返ってきたのは、もっともらしい要約でした。ところが、あとで機械的に数え直すと、指摘されなかった欠落がいくつも残っていたのです。
数えるのは機械、直し方を考えるのはエージェント。 この線引きに落ち着くまでの手順をお伝えします。
「全部見て」と頼むと、読み流されます
言語ファイルは、どれも同じ形が延々と続きます。人間が目視で追うときと同じで、モデルも長い繰り返しの中では、似た行を「問題なし」にまとめてしまいがちです。
もう一つ、見落としの性質があります。欠けているものは、そこに書かれていないので、目に入りません。キーの欠落は「ある行を読んで気づく」種類の問題ではなく、「基準と突き合わせて初めて分かる」種類の問題です。
突き合わせは、集合の差を取るだけの作業です。ここは迷わずスクリプトに任せることにしました。
機械で確かめる 4 点
検査項目は次の 4 つに絞りました。
| 項目 | 見つけるもの | 放置したときの症状 |
|---|---|---|
| missing | 基準にあって翻訳側に無いキー | 基準言語の文言が混ざって表示される |
| orphan | 翻訳側にだけ残っているキー | 表示には出ないが、棚卸しの邪魔になる |
| empty | 空文字の翻訳 | ボタンやラベルが空白になる |
| placeholder_mismatch | %1$s などの数や種類の不一致 | 数字が抜ける、型の違いで実行時エラーになる |
translatable="false" が付いたキーは、翻訳の対象外なので最初に除きます。アプリ名のように、全言語で同じ値を使うものです。
スクリプト本体
標準ライブラリだけで動きます。res ディレクトリを渡すと、言語ごとの問題を JSON で出力し、問題があれば終了コード 1 で終わります。
#!/usr/bin/env python3
"""res/values*/strings.xml の言語ずれを機械的に検出する。
使い方: python3 check_strings.py app/src/main/res
終了コード: 0=問題なし / 1=要修正あり
"""
import json
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
PLACEHOLDER = re.compile(r"%(?:\d+\$)?[-#+ 0,(]*\d*(?:\.\d+)?[sdfxXeEgGc]|%%|\{\w+\}")
def load(path):
"""name -> 文字列。translatable="false" は除外する。"""
result = {}
for node in ET.parse(path).getroot().findall("string"):
if node.get("translatable") == "false":
continue
result[node.get("name")] = "".join(node.itertext())
return result
def placeholders(text):
return sorted(PLACEHOLDER.findall(text))
def main(res_dir):
res = Path(res_dir)
base = load(res / "values" / "strings.xml")
report = {}
for path in sorted(res.glob("values-*/strings.xml")):
locale = path.parent.name.removeprefix("values-")
target = load(path)
issues = {
"missing": sorted(set(base) - set(target)),
"orphan": sorted(set(target) - set(base)),
"empty": sorted(k for k, v in target.items() if k in base and not v.strip()),
"placeholder_mismatch": sorted(
k for k, v in target.items()
if k in base and placeholders(v) != placeholders(base[k])
),
}
issues = {k: v for k, v in issues.items() if v}
if issues:
report[locale] = issues
print(json.dumps(report, ensure_ascii=False, indent=2))
return 1 if report else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("usage: check_strings.py <res_dir>")
sys.exit(main(sys.argv[1]))手元で小さなサンプルを作り、ドイツ語側にわざと 4 種類の問題を仕込んで試したところ、次のように出ました。
{
"de": {
"missing": ["favorite_added"],
"orphan": ["old_key"],
"empty": ["retry"],
"placeholder_mismatch": ["download_done"]
}
}仕込んだ問題が、そのまま 4 つ返ってきました。ここまでで、点検の「数える部分」は終わりです。
Antigravity には、結果だけを渡します
エージェントには strings.xml の全文ではなく、スクリプトの出力を渡します。依頼文は、たとえば次のようにします。
check_strings.py の出力を貼ります。
この JSON に載っているキーだけを対象に、言語ごとの修正案を出してください。
- 基準は values/strings.xml です
- placeholder_mismatch は、基準と同じプレースホルダを保ったまま訳してください
- 載っていないキーには触れないでください最後の一行が、実はいちばん効きます。範囲を JSON に載ったキーへ限ると、エージェントが「ついでに」周辺の訳を言い換えることがなくなり、差分がレビューしやすくなります。
修正が入ったら、もう一度スクリプトを走らせます。終了コードが 0 になるまでが、一巡です。
機械に任せられない判断が残ります
スクリプトが「空文字」と報告しても、それが誤りとは限りません。ある言語では敢えて何も表示しない、という判断もあり得ます。
プレースホルダの順序が言語で入れ替わるのも、正当な場合があります。%1$s と %2$d を位置指定で書いていれば、語順を変えても動きます。今回の正規表現は並べ替えて比べているので、順序の違いは不一致になりません。一方で、位置指定を使わない %s が二つ並んでいる文言は、語順を変えると意味が入れ替わるので、私は目で確認する側に回しています。
もう一点、複数形を表す plurals は、このスクリプトでは見ていません。言語によって必要な形の数が違い、単純な突き合わせでは判断できないからです。ここは別の検査が要ります。
習慣にするために
壁紙アプリは更新のたびに文言が少しずつ増えるため、言語ごとの差は放っておくと広がる一方です。私は、この検査をリリース用ブランチを切る直前の手順に入れました。終了コードを使えば、コミット前のフックにも組み込めます。
エージェントには、機械が見つけた事実だけを渡します。 事実が先にあれば、依頼文は短くなり、返ってくる修正も小さく収まります。
まずは手元のプロジェクトで、スクリプトを一度走らせてみてください。出力が空の {} だったなら、それが次回以降の基準線になります。何か出てきたなら、その行が、今日いちばん早く直せる場所です。