返回 CodeWhale
check-tui-locale-parity.py
根目录 / scripts / check-tui-locale-parity.py
1 #!/usr/bin/env python3
2 """check-tui-locale-parity.py — CI gate against TUI locale pack drift.
3
4 `en.json` is the reference pack. Every pack that claims completeness must
5 hold exact raw key-set parity with it in both directions, and every
6 `{named}` placeholder must survive translation (call sites substitute with
7 `.replace()`, so a dropped placeholder renders literal braces at runtime).
8
9 Declared partial packs (PARTIAL_PACKS, mirroring `Locale::is_partial_pack()`
10 in `crates/tui/src/localization.rs`) are exempt from completeness but must
11 not define keys English lacks — an extra key in a partial pack is drift the
12 English fallback can never surface.
13
14 This gate is the CI-visible half of the Rust parity tests
15 (`shipped_complete_packs_have_raw_key_parity_with_english`,
16 `message_id_list_english_pack_stay_in_exact_sync`). It exists so that pack
17 files on disk — including packs whose `Locale` wiring has not landed yet —
18 are held to the same contract, and so the failure is attributable to a file
19 rather than buried in a test binary.
20
21 Exits non-zero on any parity violation.
22 """
23
24 import json
25 import re
26 import sys
27 from pathlib import Path
28
29 ROOT = Path(__file__).resolve().parent.parent
30 LOCALES_DIR = ROOT / "crates" / "tui" / "locales"
31 REFERENCE = "en"
32
33 # Packs that ship deliberately incomplete, with English fallback for the
34 # missing keys. Mirrors `Locale::is_partial_pack()`. Every entry needs an
35 # issue reference; a partial pack without a tracking issue is silent drift.
36 PARTIAL_PACKS = {
37 "zh-Hant": "#4057", # Setup core only; English fallback for the rest.
38 }
39
40 PLACEHOLDER_RE = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
41
42
43 def load_pack(path: Path) -> dict:
44 try:
45 data = json.loads(path.read_text(encoding="utf-8"))
46 except (OSError, json.JSONDecodeError) as exc:
47 print(f"[tui-locale-parity] FAIL — {path.name}: unreadable JSON: {exc}")
48 sys.exit(1)
49 if not isinstance(data, dict) or not all(
50 isinstance(k, str) and isinstance(v, str) for k, v in data.items()
51 ):
52 print(f"[tui-locale-parity] FAIL — {path.name}: must be a flat string map")
53 sys.exit(1)
54 return data
55
56
57 def placeholders(value: str) -> set:
58 return set(PLACEHOLDER_RE.findall(value))
59
60
61 def main() -> int:
62 ref_path = LOCALES_DIR / f"{REFERENCE}.json"
63 if not ref_path.is_file():
64 print(f"[tui-locale-parity] FAIL — reference pack {ref_path} missing")
65 return 1
66 reference = load_pack(ref_path)
67 ref_keys = set(reference)
68 print(f"[tui-locale-parity] reference {REFERENCE}.json: {len(ref_keys)} keys")
69
70 failures = []
71 pack_files = sorted(
72 p for p in LOCALES_DIR.glob("*.json") if p.stem != REFERENCE
73 )
74 for path in pack_files:
75 tag = path.stem
76 pack = load_pack(path)
77 keys = set(pack)
78 partial_issue = PARTIAL_PACKS.get(tag)
79
80 missing = sorted(ref_keys - keys)
81 extra = sorted(keys - ref_keys)
82
83 if extra:
84 failures.append(
85 f"{tag}: defines {len(extra)} key(s) {REFERENCE}.json lacks: {extra[:10]}"
86 )
87 if partial_issue:
88 print(
89 f"[tui-locale-parity] {tag}: {len(keys)}/{len(ref_keys)} keys "
90 f"(declared partial, {partial_issue})"
91 )
92 else:
93 if missing:
94 failures.append(
95 f"{tag}: claims completeness but lacks {len(missing)} key(s); "
96 f"the English fallback hides these at runtime: {missing[:10]}"
97 )
98 # Placeholder parity only makes sense on complete packs: a
99 # partial pack legitimately omits keys wholesale.
100 for key in sorted(ref_keys & keys):
101 ref_ph = placeholders(reference[key])
102 if placeholders(pack[key]) != ref_ph:
103 failures.append(
104 f"{tag}: {key} changed placeholders "
105 f"(expected {sorted(ref_ph)}, got {sorted(placeholders(pack[key]))})"
106 )
107 if not missing:
108 print(f"[tui-locale-parity] {tag}: {len(keys)}/{len(ref_keys)} keys — complete")
109
110 if failures:
111 print("[tui-locale-parity] FAIL")
112 for failure in failures:
113 print(f" - {failure}")
114 return 1
115 print("[tui-locale-parity] PASS")
116 return 0
117
118
119 if __name__ == "__main__":
120 sys.exit(main())
121
121 lines PYTHON