828a81f3a5
- English, Spanish (/es) and Arabic (/ar, right to left) versions of every page, with a language switch - Self-hosted "I'm not a robot" check on the contact form; the message field is optional - Google Tag Manager, GA4 and Search Console verification in the shared layout - Sitemap, robots.txt and canonical links point at aigrowthtechnologies.com
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
#!/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)
|