◉ANTIGRAVITY LABEN
記事一覧/アプリ開発
▣ アプリ開発/2026-10-22中級

言語ごとの文言がいつのまにかずれていた。エージェントに点検を頼む前に走らせる、60 行の strings.xml チェック

多言語の Android アプリで、キーの欠落・空文字・プレースホルダ不一致を機械で見つける Python スクリプトと、その結果だけを Antigravity に渡して修正案を出させる運用の手順をまとめました。

Android28strings.xml多言語対応3ローカライズAntigravity394Python22

個人開発の壁紙アプリを更新する前夜、ドイツ語の端末で保存完了のメッセージを出したところ、数字の入るはずの場所が空のまま表示されました。

原因は 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 は、このスクリプトでは見ていません。言語によって必要な形の数が違い、単純な突き合わせでは判断できないからです。ここは別の検査が要ります。

習慣にするために

壁紙アプリは更新のたびに文言が少しずつ増えるため、言語ごとの差は放っておくと広がる一方です。私は、この検査をリリース用ブランチを切る直前の手順に入れました。終了コードを使えば、コミット前のフックにも組み込めます。

エージェントには、機械が見つけた事実だけを渡します。 事実が先にあれば、依頼文は短くなり、返ってくる修正も小さく収まります。

まずは手元のプロジェクトで、スクリプトを一度走らせてみてください。出力が空の {} だったなら、それが次回以降の基準線になります。何か出てきたなら、その行が、今日いちばん早く直せる場所です。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

▣ アプリ開発2026-09-19
見積書 PDF の自動化を一度にまとめて頼み、4 つに割り直すまで
受託の見積書を毎回コピーして作っていた手作業を、Antigravity に頼んで小さな PDF 出力ツールへ移した記録です。最初に全部をまとめて頼んで作り直すことになった理由と、データ・計算・見た目・保存の 4 つに割って渡し直した順番を、動くコードと一緒に書き残します。
▣ アプリ開発2026-05-25
Antigravity の Inline Edit と Agent モードを壁紙アプリ運用で1ヶ月使い分けた所感
壁紙アプリ4本の実運用で Antigravity の Inline Edit と Agent モードを1ヶ月使い分けた所感です。実測値・AGENTS.md への判断軸の固定・dSYM 一括統一・クレジット消費まで、現実的な運用設計を共有します。
▣ アプリ開発2026-10-20
PHP の API が no-store を返し、Cloudflare は素通しでした。エッジの TTL だけを上書きした設計メモ
壁紙アプリの一覧 API が no-store を返していたため、Cloudflare の前に置いても毎回オリジンまで届いていました。端末側の挙動は変えずにエッジの TTL だけを上書きする Cache Rule、効いたかを確かめる検査スクリプト、古い応答を出さないための「アップロードとパージを一組にする」手順までを書き残します。