Files
aigrowthtech/scripts/i18n.py
T
Alejandro Veras 5bf472b735 Serve the site in English, Spanish and Arabic, add a captcha, make the message optional
- Move every route under app/[lang]; a proxy keeps English on unprefixed URLs and serves /es and /ar
- Interface strings and homepage copy per locale; content pages load a translated JSON when one exists
- Language switch in the header, hreflang alternates and a multilingual sitemap
- Right-to-left layout for Arabic with logical CSS properties, Noto Sans Arabic and no letter-spacing
- Self-hosted proof-of-work "I'm not a robot" check (no third-party account) replaces Turnstile
- The message field is optional; failed attempts refill the form instead of clearing it
2026-10-13 10:49:14 -04:00

131 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Translation helpers for the generated content pages.
python3 -E scripts/i18n.py extract <out_dir>
Write one flat {path: text} JSON per English page with only the strings to translate.
python3 -E scripts/i18n.py apply <locale> <dir>
Read translated flat files from <dir> and write src/content/pages/<locale>/<slug>.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)