返回 CodeWhale
check-readme-translations.py
根目录 / scripts / check-readme-translations.py
1 #!/usr/bin/env python3
2 """Keep the localized READMEs in lockstep with README.md.
3
4 Every translated README must:
5 1. carry a source stamp `<!-- source: README.md sha256:<12-hex> -->`
6 matching the current hash of README.md — so any English edit fails CI
7 until the translations are refreshed;
8 2. contain exactly the same fenced code blocks, in the same order
9 (commands are never translated);
10 3. contain every non-language-switcher URL the English README contains;
11 4. have the same number of `##` sections.
12
13 Run: python3 scripts/check-readme-translations.py
14 """
15
16 from __future__ import annotations
17
18 import hashlib
19 import re
20 import sys
21 from pathlib import Path
22
23 ROOT = Path(__file__).resolve().parent.parent
24 SOURCE = ROOT / "README.md"
25 TRANSLATIONS = [
26 "README.zh-CN.md",
27 "README.ja-JP.md",
28 "README.vi.md",
29 "README.id.md",
30 "README.ko-KR.md",
31 "README.es-419.md",
32 "README.pt-BR.md",
33 "README.ru.md",
34 "README.uk.md",
35 ]
36 STAMP_RE = re.compile(r"<!--\s*source:\s*README\.md\s+sha256:([0-9a-f]{12})\s*-->")
37 FENCE_RE = re.compile(r"```[a-z]*\n(.*?)```", re.DOTALL)
38 URL_RE = re.compile(r"\((https?://[^)\s]+|docs/[^)\s]+|[A-Za-z0-9_./-]+\.md[^)\s]*)\)")
39 # The language-switcher line legitimately differs per translation (each file
40 # links the *other* languages), so its links are exempt from the URL check.
41 LANGUAGE_LINKS = {
42 "README.md",
43 "README.zh-CN.md",
44 "README.ja-JP.md",
45 "README.vi.md",
46 "README.id.md",
47 "README.ko-KR.md",
48 "README.es-419.md",
49 "README.pt-BR.md",
50 "README.ru.md",
51 "README.uk.md",
52 }
53
54
55 def source_stamp() -> str:
56 return hashlib.sha256(SOURCE.read_bytes()).hexdigest()[:12]
57
58
59 def fences(text: str) -> list[str]:
60 return [m.strip() for m in FENCE_RE.findall(text)]
61
62
63 def urls(text: str) -> set[str]:
64 return {u for u in URL_RE.findall(text) if u not in LANGUAGE_LINKS}
65
66
67 def sections(text: str) -> int:
68 return len(re.findall(r"^## ", text, re.MULTILINE))
69
70
71 def main() -> int:
72 expected = source_stamp()
73 en = SOURCE.read_text()
74 en_fences = fences(en)
75 en_urls = urls(en)
76 en_sections = sections(en)
77 failures: list[str] = []
78
79 for name in TRANSLATIONS:
80 path = ROOT / name
81 if not path.exists():
82 failures.append(f"{name}: missing")
83 continue
84 text = path.read_text()
85
86 stamp = STAMP_RE.search(text)
87 if not stamp:
88 failures.append(
89 f"{name}: no source stamp — add "
90 f"'<!-- source: README.md sha256:{expected} -->'"
91 )
92 elif stamp.group(1) != expected:
93 failures.append(
94 f"{name}: stale (stamped {stamp.group(1)}, README.md is now "
95 f"{expected}) — retranslate, then update the stamp"
96 )
97
98 tr_fences = fences(text)
99 if tr_fences != en_fences:
100 failures.append(
101 f"{name}: code blocks differ from README.md "
102 f"({len(tr_fences)} vs {len(en_fences)}; commands must never "
103 f"be translated or reordered)"
104 )
105
106 missing = en_urls - urls(text)
107 if missing:
108 failures.append(f"{name}: missing links: {sorted(missing)[:5]}")
109
110 if sections(text) != en_sections:
111 failures.append(
112 f"{name}: {sections(text)} '##' sections vs README.md's "
113 f"{en_sections}"
114 )
115
116 if failures:
117 print("README translation check FAILED:")
118 for f in failures:
119 print(f" - {f}")
120 print(f"\nCurrent README.md stamp: sha256:{expected}")
121 return 1
122
123 print(
124 f"README translation check OK — {len(TRANSLATIONS)} translations in "
125 f"sync with README.md (sha256:{expected})"
126 )
127 return 0
128
129
130 if __name__ == "__main__":
131 sys.exit(main())
132
132 lines PYTHON