aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/docs_translation_parity.py145
-rw-r--r--scripts/test_docs_translation_parity.py78
2 files changed, 223 insertions, 0 deletions
diff --git a/scripts/docs_translation_parity.py b/scripts/docs_translation_parity.py
new file mode 100644
index 00000000..6b15441b
--- /dev/null
+++ b/scripts/docs_translation_parity.py
@@ -0,0 +1,145 @@
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+
+DEFAULT_LOCALES = ("ja", "zh-cn")
+
+
+def heading_outline(path: Path) -> list[int]:
+ outline: list[int] = []
+ in_fence = False
+
+ for line in path.read_text(encoding="utf-8").splitlines():
+ stripped = line.lstrip()
+ if stripped.startswith("```") or stripped.startswith("~~~"):
+ in_fence = not in_fence
+ continue
+ if in_fence or not stripped.startswith("#"):
+ continue
+
+ level = 0
+ for char in stripped:
+ if char != "#":
+ break
+ level += 1
+
+ if level == 0 or level > 6:
+ continue
+ if len(stripped) > level and stripped[level] not in (" ", "\t"):
+ continue
+
+ outline.append(level)
+
+ return outline
+
+
+def english_docs(docs_root: Path) -> list[Path]:
+ return sorted(
+ path
+ for path in docs_root.glob("*.mdx")
+ if path.is_file()
+ )
+
+
+def locale_docs(docs_root: Path, locale: str) -> list[Path]:
+ locale_root = docs_root / locale
+ if not locale_root.exists():
+ return []
+ return sorted(path for path in locale_root.glob("*.mdx") if path.is_file())
+
+
+def check_docs_translation_parity(docs_root: Path, locales: tuple[str, ...] = DEFAULT_LOCALES) -> list[str]:
+ errors: list[str] = []
+ english = english_docs(docs_root)
+ english_names = {path.name for path in english}
+
+ for locale in locales:
+ translated_names = {path.name for path in locale_docs(docs_root, locale)}
+
+ for missing in sorted(english_names - translated_names):
+ errors.append(f"{docs_root / locale / missing}: missing translation file")
+
+ for stale in sorted(translated_names - english_names):
+ errors.append(f"{docs_root / locale / stale}: no matching English doc")
+
+ for source in english:
+ source_outline = heading_outline(source)
+
+ for locale in locales:
+ translated = docs_root / locale / source.name
+ if not translated.exists():
+ continue
+
+ translated_outline = heading_outline(translated)
+ if translated_outline == source_outline:
+ continue
+
+ errors.append(
+ format_outline_error(source, translated, source_outline, translated_outline)
+ )
+
+ return errors
+
+
+def format_outline_error(
+ source: Path,
+ translated: Path,
+ source_outline: list[int],
+ translated_outline: list[int],
+) -> str:
+ return (
+ f"{translated}: heading outline differs from {source} "
+ f"(English {format_counts(source_outline)}, translated {format_counts(translated_outline)})"
+ )
+
+
+def format_counts(levels: list[int]) -> str:
+ if not levels:
+ return "0 headings"
+
+ parts = []
+ for level in range(1, 7):
+ count = levels.count(level)
+ if count:
+ parts.append(f"h{level}={count}")
+ return ", ".join(parts)
+
+
+def parse_args(argv: list[str]) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Check localized docs have the same heading outline as English docs."
+ )
+ parser.add_argument(
+ "--docs-root",
+ default="website/src/content/docs",
+ type=Path,
+ help="Docs content root containing English .mdx files and locale subdirectories.",
+ )
+ parser.add_argument(
+ "--locale",
+ action="append",
+ dest="locales",
+ help="Locale subdirectory to check. Can be passed more than once.",
+ )
+ return parser.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = parse_args(sys.argv[1:] if argv is None else argv)
+ locales = tuple(args.locales or DEFAULT_LOCALES)
+ errors = check_docs_translation_parity(args.docs_root, locales)
+
+ if errors:
+ print("error: localized docs heading outlines differ from English docs", file=sys.stderr)
+ for error in errors:
+ print(f"- {error}", file=sys.stderr)
+ return 1
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/test_docs_translation_parity.py b/scripts/test_docs_translation_parity.py
new file mode 100644
index 00000000..9a8aa378
--- /dev/null
+++ b/scripts/test_docs_translation_parity.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from scripts.docs_translation_parity import check_docs_translation_parity, heading_outline
+
+
+class DocsTranslationParityTests(unittest.TestCase):
+ def test_heading_outline_ignores_fenced_code_blocks(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "doc.mdx"
+ path.write_text(
+ "# Title\n\n```md\n## Not a heading\n```\n\n## Real section\n",
+ encoding="utf-8",
+ )
+
+ self.assertEqual(heading_outline(path), [1, 2])
+
+ def test_parity_accepts_translated_heading_text_with_same_shape(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ (root / "ja").mkdir()
+ (root / "zh-cn").mkdir()
+ (root / "guide.mdx").write_text("# Guide\n\n## Install\n\n### Verify\n", encoding="utf-8")
+ (root / "ja" / "guide.mdx").write_text(
+ "# ガイド\n\n## インストール\n\n### 確認\n",
+ encoding="utf-8",
+ )
+ (root / "zh-cn" / "guide.mdx").write_text(
+ "# 指南\n\n## 安装\n\n### 验证\n",
+ encoding="utf-8",
+ )
+
+ self.assertEqual(check_docs_translation_parity(root), [])
+
+ def test_parity_reports_missing_heading_sections(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ (root / "ja").mkdir()
+ (root / "zh-cn").mkdir()
+ (root / "cli-reference.mdx").write_text(
+ "# CLI reference\n\n## Launch\n\n## Shell completions\n",
+ encoding="utf-8",
+ )
+ (root / "ja" / "cli-reference.mdx").write_text(
+ "# CLI リファレンス\n\n## 起動\n",
+ encoding="utf-8",
+ )
+ (root / "zh-cn" / "cli-reference.mdx").write_text(
+ "# CLI 参考\n\n## 启动\n\n## Shell 补全\n",
+ encoding="utf-8",
+ )
+
+ errors = check_docs_translation_parity(root)
+
+ self.assertEqual(len(errors), 1)
+ self.assertIn("ja/cli-reference.mdx", errors[0])
+ self.assertIn("heading outline differs", errors[0])
+
+ def test_parity_reports_missing_and_stale_files(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ (root / "ja").mkdir()
+ (root / "zh-cn").mkdir()
+ (root / "guide.mdx").write_text("# Guide\n", encoding="utf-8")
+ (root / "ja" / "old.mdx").write_text("# Old\n", encoding="utf-8")
+ (root / "zh-cn" / "guide.mdx").write_text("# 指南\n", encoding="utf-8")
+
+ errors = check_docs_translation_parity(root)
+
+ self.assertIn(f"{root / 'ja' / 'guide.mdx'}: missing translation file", errors)
+ self.assertIn(f"{root / 'ja' / 'old.mdx'}: no matching English doc", errors)
+
+
+if __name__ == "__main__":
+ unittest.main()