返回 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 "README.fr.md",
36 "README.de.md",
37 "README.zh-TW.md",
38 "README.hi.md",
39 "README.tr.md",
40 "README.it.md",
41 "README.pl.md",
42 "README.ar.md",
43 "README.ca.md",
44 ]
45 STAMP_RE = re.compile(r"<!--\s*source:\s*README\.md\s+sha256:([0-9a-f]{12})\s*-->")
46 FENCE_RE = re.compile(r"```[a-z]*\n(.*?)```", re.DOTALL)
47 URL_RE = re.compile(r"\((https?://[^)\s]+|docs/[^)\s]+|[A-Za-z0-9_./-]+\.md[^)\s]*)\)")
48 # The language-switcher line legitimately differs per translation (each file
49 # links the *other* languages), so its links are exempt from the URL check.
50 LANGUAGE_LINKS = {
51 "README.md",
52 "README.zh-CN.md",
53 "README.ja-JP.md",
54 "README.vi.md",
55 "README.id.md",
56 "README.ko-KR.md",
57 "README.es-419.md",
58 "README.pt-BR.md",
59 "README.ru.md",
60 "README.uk.md",
61 "README.fr.md",
62 "README.de.md",
63 "README.zh-TW.md",
64 "README.hi.md",
65 "README.tr.md",
66 "README.it.md",
67 "README.pl.md",
68 "README.ar.md",
69 "README.ca.md",
70 }
71
72
73 def source_stamp() -> str:
74 return hashlib.sha256(SOURCE.read_bytes()).hexdigest()[:12]
75
76
77 def fences(text: str) -> list[str]:
78 return [m.strip() for m in FENCE_RE.findall(text)]
79
80
81 def urls(text: str) -> set[str]:
82 return {u for u in URL_RE.findall(text) if u not in LANGUAGE_LINKS}
83
84
85 def sections(text: str) -> int:
86 return len(re.findall(r"^## ", text, re.MULTILINE))
87
88
89 def main() -> int:
90 expected = source_stamp()
91 en = SOURCE.read_text()
92 en_fences = fences(en)
93 en_urls = urls(en)
94 en_sections = sections(en)
95 failures: list[str] = []
96
97 for name in TRANSLATIONS:
98 path = ROOT / name
99 if not path.exists():
100 failures.append(f"{name}: missing")
101 continue
102 text = path.read_text()
103
104 stamp = STAMP_RE.search(text)
105 if not stamp:
106 failures.append(
107 f"{name}: no source stamp — add "
108 f"'<!-- source: README.md sha256:{expected} -->'"
109 )
110 elif stamp.group(1) != expected:
111 failures.append(
112 f"{name}: stale (stamped {stamp.group(1)}, README.md is now "
113 f"{expected}) — retranslate, then update the stamp"
114 )
115
116 tr_fences = fences(text)
117 if tr_fences != en_fences:
118 failures.append(
119 f"{name}: code blocks differ from README.md "
120 f"({len(tr_fences)} vs {len(en_fences)}; commands must never "
121 f"be translated or reordered)"
122 )
123
124 missing = en_urls - urls(text)
125 if missing:
126 failures.append(f"{name}: missing links: {sorted(missing)[:5]}")
127
128 if sections(text) != en_sections:
129 failures.append(
130 f"{name}: {sections(text)} '##' sections vs README.md's "
131 f"{en_sections}"
132 )
133
134 if failures:
135 print("README translation check FAILED:")
136 for f in failures:
137 print(f" - {f}")
138 print(f"\nCurrent README.md stamp: sha256:{expected}")
139 return 1
140
141 print(
142 f"README translation check OK — {len(TRANSLATIONS)} translations in "
143 f"sync with README.md (sha256:{expected})"
144 )
145 return 0
146
147
148 if __name__ == "__main__":
149 sys.exit(main())
150
150 lines PYTHON