#!/usr/bin/env python3 """Translation helpers for the generated content pages. python3 -E scripts/i18n.py extract Write one flat {path: text} JSON per English page with only the strings to translate. python3 -E scripts/i18n.py apply Read translated flat files from and write src/content/pages//.json, keeping the English structure. Fails if a translation is missing or extra. python3 -E scripts/i18n.py index Regenerate src/content/pages/index.ts. A locale without a page falls back to English. """ from __future__ import annotations import copy import json import pathlib import re import sys ROOT = pathlib.Path(__file__).resolve().parent.parent PAGES = ROOT / "src" / "content" / "pages" LOCALES = ["en", "es", "ar"] SKIP_KEYS = {"slug", "path", "form", "level", "kind", "num"} def keep_english(value: str, section: dict | None, key: str) -> bool: """Office addresses and stat figures stay as they are in every language.""" if "Phone:" in value: return True if section and section.get("kind") == "stats" and key == "title" and re.match(r"^[\d.,]+", value): return True return False def walk(node, path=(), section=None): if isinstance(node, dict): if "kind" in node: section = node for k, v in node.items(): if k in SKIP_KEYS: continue yield from walk(v, path + (k,), section) elif isinstance(node, list): for i, v in enumerate(node): yield from walk(v, path + (str(i),), section) elif isinstance(node, str) and node.strip(): if not keep_english(node, section, path[-1]): yield ".".join(path), node def set_path(node, path: str, value: str): parts = path.split(".") for p in parts[:-1]: node = node[int(p)] if isinstance(node, list) else node[p] last = parts[-1] if isinstance(node, list): node[int(last)] = value else: node[last] = value def english_pages(): return sorted((PAGES / "en").glob("*.json")) def extract(out_dir: str): out = pathlib.Path(out_dir) out.mkdir(parents=True, exist_ok=True) for f in english_pages(): flat = dict(walk(json.loads(f.read_text()))) (out / f.name).write_text(json.dumps(flat, ensure_ascii=False, indent=1) + "\n") print(f"{f.name}: {len(flat)} strings") def apply(locale: str, src_dir: str): src = pathlib.Path(src_dir) dest = PAGES / locale dest.mkdir(parents=True, exist_ok=True) problems = 0 for f in english_pages(): tf = src / f.name if not tf.exists(): print(f"skip {f.name}: no translation yet") continue page = json.loads(f.read_text()) wanted = dict(walk(page)) got = json.loads(tf.read_text()) missing = [k for k in wanted if k not in got] extra = [k for k in got if k not in wanted] if missing or extra: problems += 1 print(f"{f.name}: missing {missing[:5]} extra {extra[:5]}") continue out = copy.deepcopy(page) for k, v in got.items(): set_path(out, k, v) (dest / f.name).write_text(json.dumps(out, ensure_ascii=False, indent=2) + "\n") print(f"{locale}/{f.name}: ok") if problems: sys.exit(1) def ident(locale: str, name: str) -> str: return locale + "_" + re.sub(r"\W", "_", name) def index(): names = [f.stem for f in english_pages()] lines = ["// Generated by scripts/i18n.py index — do not edit by hand.", ""] table = [] for locale in LOCALES: entries = [] for n in names: if (PAGES / locale / f"{n}.json").exists(): lines.append(f'import {ident(locale, n)} from "./{locale}/{n}.json";') entries.append(f' "{n}": {ident(locale, n)},') else: entries.append(f' "{n}": {ident("en", n)},') table.append(f" {locale}: {{\n" + "\n".join(entries) + "\n },") lines += ["", "export const content = {", *table, "};", ""] (PAGES / "index.ts").write_text("\n".join(lines)) print(f"index.ts: {len(names)} pages × {len(LOCALES)} locales") if __name__ == "__main__": cmd, *args = sys.argv[1:] {"extract": extract, "apply": apply, "index": index}[cmd](*args)