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
This commit is contained in:
@@ -7,7 +7,7 @@ Usage:
|
||||
Paragraphs that start with "> " were bold sentences in the deck and render as
|
||||
emphasised lines. Each document follows the same conventions: bold all-caps lines are eyebrows
|
||||
(when followed by a heading) or CTAs; Heading 1/2/3 give the structure.
|
||||
The output lands in src/content/pages/<slug>.json. The homepage is hand-built
|
||||
The output lands in src/content/pages/en/<slug>.json. The homepage is hand-built
|
||||
and is skipped.
|
||||
"""
|
||||
|
||||
@@ -18,7 +18,7 @@ import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
OUT = pathlib.Path(__file__).resolve().parent.parent / "src" / "content" / "pages"
|
||||
OUT = pathlib.Path(__file__).resolve().parent.parent / "src" / "content" / "pages" / "en"
|
||||
|
||||
# Lines in the decks that are notes to the team, not site copy.
|
||||
DROP = [
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user