#!/usr/bin/env python3"""res/values 以下の文字列リソースを HEAD と突き合わせ、エージェントの編集による翻訳ドリフトを検出する pre-commit ゲート。"""import reimport subprocessimport sysimport xml.etree.ElementTree as ETfrom pathlib import Path# 既定(source)の値と一致してよい鍵(ブランド名・記号的に原文一致が正のもの)ALLOW_EQUAL = {"app_name", "ok_label", "brand_tagline"}# %1$s / %d / %@ / {count} を順不同の多重集合へ正規化するARG_RE = re.compile(r"%(?:\d+\$)?[-#+ 0,(]*\d*(?:\.\d+)?([@a-zA-Z])|\{(\w+)\}")def format_args(value: str): args = [] for m in ARG_RE.finditer(value): if m.group(2) is not None: # ICU 形式 {name} args.append("{}") else: # printf 系 %1$s / %d / %@ args.append("%" + m.group(1)) return sorted(args)def parse_strings(xml_text: str): """strings.xml の本文を name->value 辞書に。plurals は name#quantity。""" out = {} if not xml_text.strip(): return out root = ET.fromstring(xml_text) for s in root.findall("string"): name = s.get("name") if name and s.get("translatable") != "false": out[name] = "".join(s.itertext()) for p in root.findall("plurals"): name = p.get("name") for item in p.findall("item"): out[f"{name}#{item.get('quantity')}"] = "".join(item.itertext()) return outdef at_head(rel_path: str): """HEAD 時点の同ファイルを取得(新規ファイルは空辞書)。""" r = subprocess.run(["git", "show", f"HEAD:{rel_path}"], capture_output=True, text=True) return parse_strings(r.stdout) if r.returncode == 0 else {}
戻し訳の判定がいちばん要です。「いまの値が原文と一致し、かつ HEAD の時点では原文と違っていた」ときだけ違反とします。最初から原文と同じだった鍵(短い記号やブランド名)は対象外になり、過去に訳されていた一行が原文へ戻った瞬間だけを捕まえられます。
def check_locale(default_now, base_loc, cur_loc, locale): v = [] # 1. 欠落: HEAD にあった鍵が、いまのロケールから消えている for key in base_loc.keys() - cur_loc.keys(): v.append((locale, key, "removed", "HEAD にあった鍵が消えています")) # 2. 戻し訳: 訳済みだった値が、既定言語の原文へ戻っている for key in cur_loc.keys() & base_loc.keys(): src = default_now.get(key) if src is None or key in ALLOW_EQUAL: continue if cur_loc[key] == src and base_loc[key] != src: v.append((locale, key, "reverted", "訳が既定言語の原文へ戻っています")) # 3. 書式引数: 個数・種類が既定と一致しない(クラッシュ源) for key, val in cur_loc.items(): src = default_now.get(key) if src and format_args(val) != format_args(src): v.append((locale, key, "format", f"書式引数が不一致 {format_args(src)} != {format_args(val)}")) return v