Check translation contracts offline, add Swedish to the installer and enable the website languages

- Offline checks for terminal and website catalogs reject translations that change commands, paths, identifiers or placeholders
- Terminal and documentation texts that broke those contracts are corrected in every language
- Swedish can be chosen in the stable and beta installers
- The website routes, language switcher and exported pages cover the eight languages, with the shared website texts in each of them
- Website documentation translations are generated only when requested
- From PR #475 by @jcastro
This commit is contained in:
MacRimi
2026-10-18 12:58:25 +02:00
parent f363ef4395
commit f8cabb4700
54 changed files with 2149 additions and 173 deletions
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Check documentation translation structure and literal contracts offline.
Missing locales/leaves are allowed by default because the website falls back
to English. Use --require-complete when validating a catalog completion PR.
This checks data integrity, not translation fluency or linguistic accuracy.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from build_web_docs_i18n import (
DEFAULT_LANGUAGES,
get_at_path,
iter_leaves,
literal_values_match,
read_json,
schema_matches,
source_files,
)
from check_terminal_catalogs import validate_message
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--messages-dir", type=Path, default=Path("web/messages"))
parser.add_argument("--languages", default=",".join(DEFAULT_LANGUAGES))
parser.add_argument("--require-complete", action="store_true")
args = parser.parse_args()
root = args.messages_dir.resolve()
files = source_files(root / "en", ".")
errors = 0
for language in args.languages.split(","):
language = language.strip()
if language not in DEFAULT_LANGUAGES:
parser.error(f"Unsupported locale: {language}")
missing = checked = present_files = 0
for file in files:
relative = file.relative_to(root / "en")
source = read_json(file)
target = read_json(root / language / relative)
if target is not None:
present_files += 1
if args.require_complete and not schema_matches(source, target):
print(f"[{language}] {relative}: source/target schema differs")
errors += 1
for leaf in iter_leaves(source):
localized = get_at_path(target, leaf.path)
if not isinstance(localized, str) or (leaf.source.strip() and not localized.strip()):
missing += 1
continue
try:
validate_message(
leaf.source, localized,
title_separator=bool(
leaf.path and leaf.path[0] == "meta"
and leaf.path[-1] in ("title", "ogTitle", "twitterTitle")
),
)
except ValueError as exc:
print(f"[{language}] {relative}:{'.'.join(map(str, leaf.path))}: {exc}")
errors += 1
checked += 1
if args.require_complete and not literal_values_match(source, target):
print(f"[{language}] {relative}: literal commands or rendering metadata differ")
errors += 1
print(f"[{language}] files={present_files}/{len(files)} strings={checked} missing={missing}")
if args.require_complete and missing:
errors += 1
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())