返回 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" / "localization" / "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 # Empty since #5143 brought `zh-Hant` to full `en.json` parity and
37 # `Locale::is_partial_pack()` began returning false for every shipped locale.
38 PARTIAL_PACKS: dict[str, str] = {}
39
40 PLACEHOLDER_RE = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
41
42
43 def unique_keys(pairs: list[tuple[str, str]]) -> dict:
44 data = {}
45 for key, value in pairs:
46 if key in data:
47 raise ValueError(f"duplicate key: {key}")
48 data[key] = value
49 return data
50
51
52 def load_pack(path: Path) -> dict:
53 try:
54 data = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_keys)
55 except (OSError, ValueError) as exc:
56 print(f"[tui-locale-parity] FAIL — {path.name}: unreadable JSON: {exc}")
57 sys.exit(1)
58 if not isinstance(data, dict) or not all(
59 isinstance(k, str) and isinstance(v, str) for k, v in data.items()
60 ):
61 print(f"[tui-locale-parity] FAIL — {path.name}: must be a flat string map")
62 sys.exit(1)
63 return data
64
65
66 def placeholders(value: str) -> set:
67 return set(PLACEHOLDER_RE.findall(value))
68
69
70 def main() -> int:
71 ref_path = LOCALES_DIR / f"{REFERENCE}.json"
72 if not ref_path.is_file():
73 print(f"[tui-locale-parity] FAIL — reference pack {ref_path} missing")
74 return 1
75 reference = load_pack(ref_path)
76 ref_keys = set(reference)
77 print(f"[tui-locale-parity] reference {REFERENCE}.json: {len(ref_keys)} keys")
78
79 failures = []
80 pack_files = sorted(
81 p for p in LOCALES_DIR.glob("*.json") if p.stem != REFERENCE
82 )
83 for path in pack_files:
84 tag = path.stem
85 pack = load_pack(path)
86 keys = set(pack)
87 partial_issue = PARTIAL_PACKS.get(tag)
88
89 missing = sorted(ref_keys - keys)
90 extra = sorted(keys - ref_keys)
91
92 if extra:
93 failures.append(
94 f"{tag}: defines {len(extra)} key(s) {REFERENCE}.json lacks: {extra[:10]}"
95 )
96 if partial_issue:
97 print(
98 f"[tui-locale-parity] {tag}: {len(keys)}/{len(ref_keys)} keys "
99 f"(declared partial, {partial_issue})"
100 )
101 else:
102 if missing:
103 failures.append(
104 f"{tag}: claims completeness but lacks {len(missing)} key(s); "
105 f"the English fallback hides these at runtime: {missing[:10]}"
106 )
107 # Placeholder parity only makes sense on complete packs: a
108 # partial pack legitimately omits keys wholesale.
109 for key in sorted(ref_keys & keys):
110 ref_ph = placeholders(reference[key])
111 if placeholders(pack[key]) != ref_ph:
112 failures.append(
113 f"{tag}: {key} changed placeholders "
114 f"(expected {sorted(ref_ph)}, got {sorted(placeholders(pack[key]))})"
115 )
116 if not missing:
117 print(f"[tui-locale-parity] {tag}: {len(keys)}/{len(ref_keys)} keys — complete")
118
119 if failures:
120 print("[tui-locale-parity] FAIL")
121 for failure in failures:
122 print(f" - {failure}")
123 return 1
124 print("[tui-locale-parity] PASS")
125 return 0
126
127
128 if __name__ == "__main__":
129 sys.exit(main())
130
130 lines PYTHON