Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ Per-item detail lives in the dated work log below. **Since 2026-07 versions are

---

### ★ Website translated into all 13 game languages (2026-08-10, branch feat/wix-i18n-locale-tools)
The full website (all pages, menu, forms, image alt texts) now has complete translations in every
game language: en/it plus newly created es, fr, nl, pl, pt, tr, ru, uk, ja, ko, zh — the 11 new Wix
locales were created **HIDDEN** and stay invisible until reviewed and flipped visible. Translations
were produced locally (one Claude subagent per language, game locale files as terminology glossary,
EN as meaning reference, link-/markup-preserving checks + dry-run before every import); Wix machine
translation and its word credits were not used. Tooling added: `create_locales.py` (create HIDDEN
secondary locales), `curate_todo.py` (filter template leftovers out of audit todos),
`import_translations.py` now copies the required `parentEntityId` from the primary-language record
on create. Remaining editor-only tasks: per-language SEO meta/slugs, and the EN school-club page
additions (dataquery links are numbered per language, so link-bearing sections can't be appended via
API into an existing translation).

### ★ Website i18n tooling: Wix translation audit + import (2026-08-10, branch feat/wix-i18n-audit)
`tools/wix-i18n/` — manage the website's translations via the Wix Multilingual REST APIs, bypassing
Wix's machine translation. `audit_translations.py` (read-only) dumps schemas + per-locale contents and
Expand Down
37 changes: 37 additions & 0 deletions tools/wix-i18n/create_locales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["requests"]
# ///
"""Create secondary site locales (HIDDEN) for the given language codes.

Skips locales that already exist. Usage:
uv run create_locales.py es fr nl pl pt tr ru uk ja ko zh
"""
import sys

from audit_translations import API, fetch_locales, load_env, make_session

sys.stdout.reconfigure(encoding="utf-8")


def main() -> None:
codes = sys.argv[1:]
if not codes:
sys.exit("usage: create_locales.py <languageCode> [...]")
s = make_session(load_env())
existing = {l["languageCode"] for l in fetch_locales(s)}
for code in codes:
if code in existing:
print(f"{code}: exists, skipped")
continue
r = s.post(f"{API}/locales/v2/locale",
json={"locale": {"languageCode": code, "visibility": "HIDDEN"}})
if r.status_code == 200:
loc = r.json()["locale"]
print(f"{code}: created HIDDEN (id={loc['id']}, display={loc.get('effectiveDisplayName')})")
else:
print(f"{code}: FAILED {r.status_code} {r.text[:200]}")


if __name__ == "__main__":
main()
44 changes: 44 additions & 0 deletions tools/wix-i18n/curate_todo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# /// script
# requires-python = ">=3.11"
# ///
"""Filter a todo-<locale>.json down to real, visitor-facing content.

Drops known template leftovers: items whose schema no longer exists on the
site (deleted template collections), the template job-application form, the
Translation-Manager pseudo page title and "Image Title" placeholder values.

Usage: uv run curate_todo.py es fr ... (writes out/todo-<locale>-curated.json)
"""
import json
import sys
from pathlib import Path

sys.stdout.reconfigure(encoding="utf-8")
OUT = Path(__file__).resolve().parent / "out"

TEMPLATE_ENTITIES = {
"fde85b74-ac43-4dc1-aa38-c47fee4407fb", # Bewerbungsformular (template job form)
"masterPage",
}
PLACEHOLDER_TEXTS = {"Image Title"}


def curate(locale: str) -> None:
todo = json.loads((OUT / f"todo-{locale}.json").read_text(encoding="utf-8"))
curated = []
for item in todo:
if item["schema"] == "unknown schema" or item["entityId"] in TEMPLATE_ENTITIES:
continue
fields = {k: f for k, f in item["fields"].items()
if f["text"].strip() not in PLACEHOLDER_TEXTS}
if fields:
curated.append({**item, "fields": fields})
n_fields = sum(len(i["fields"]) for i in curated)
chars = sum(len(f["text"]) for i in curated for f in i["fields"].values())
(OUT / f"todo-{locale}-curated.json").write_text(
json.dumps(curated, indent=1, ensure_ascii=False), encoding="utf-8")
print(f"{locale}: {len(curated)} items, {n_fields} fields, {chars} chars")


for loc in sys.argv[1:]:
curate(loc)
25 changes: 17 additions & 8 deletions tools/wix-i18n/import_translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
from audit_translations import API, load_env, make_session, text_preview


def fetch_existing_keys(s: requests.Session, locale: str) -> set[tuple[str, str]]:
keys: set[tuple[str, str]] = set()
def fetch_contents_map(s: requests.Session, locale: str) -> dict[tuple[str, str], dict]:
contents: dict[tuple[str, str], dict] = {}
cursor = None
while True:
paging: dict = {"limit": 100}
Expand All @@ -44,11 +44,11 @@ def fetch_existing_keys(s: requests.Session, locale: str) -> set[tuple[str, str]
r = s.post(f"{API}/translation-content/v1/contents/search", json=body)
r.raise_for_status()
d = r.json()
keys |= {(c["schemaId"], c["entityId"]) for c in d.get("contents", [])}
contents.update({(c["schemaId"], c["entityId"]): c for c in d.get("contents", [])})
pm = d.get("pagingMetadata", {})
cursor = pm.get("cursors", {}).get("next")
if not pm.get("hasNext"):
return keys
return contents


def field_text(f: dict, base_dir: Path) -> str:
Expand All @@ -58,14 +58,18 @@ def field_text(f: dict, base_dir: Path) -> str:
return f["text"]


def content_payload(item: dict, locale: str, base_dir: Path) -> dict:
return {
def content_payload(item: dict, locale: str, base_dir: Path,
parent_entity_id: str | None = None) -> dict:
payload = {
"schemaId": item["schemaId"],
"entityId": item["entityId"],
"locale": locale,
"fields": {key: {"textValue": field_text(f, base_dir), "published": True}
for key, f in item["fields"].items()},
}
if parent_entity_id:
payload["parentEntityId"] = parent_entity_id
return payload


def main() -> None:
Expand All @@ -85,7 +89,10 @@ def main() -> None:

env = load_env()
s = make_session(env)
existing = fetch_existing_keys(s, locale)
existing = set(fetch_contents_map(s, locale))
# Some schemas (editor components) require the parent entity (page) on create;
# copy it from the primary-language record.
primary = fetch_contents_map(s, "de")

creates = [i for i in items if (i["schemaId"], i["entityId"]) not in existing]
updates = [i for i in items if (i["schemaId"], i["entityId"]) in existing]
Expand All @@ -103,8 +110,10 @@ def main() -> None:

failures = 0
for item in creates:
de_record = primary.get((item["schemaId"], item["entityId"]), {})
r = s.post(f"{API}/translation-content/v1/contents",
json={"content": content_payload(item, locale, base_dir)})
json={"content": content_payload(item, locale, base_dir,
de_record.get("parentEntityId"))})
if r.status_code != 200:
failures += 1
print(f"FAILED create {item['schema']} {item['entityId']}: "
Expand Down