Internationalization

Games use i18next keys instead of inline English strings.

Files

Path Content
locales/en/apgames.json English source — game names (names), descriptions, UI strings, category tag labels (categories)
locales/en/apresults.json English source — move result / chat log strings

Locale JSON on disk uses 2-space indentation and a trailing newline. Scripts (translate, prune-locales, sync-game-names-locale, etc.) share scripts/locale-json-format.mjs; npm run lint runs check-locale-json-format so manual edits stay aligned with --write tools. | locales/{de,fr,it,es-US}/*.json | Translations only (Weblate + CI) | | locale-src/{de,fr,it,es-US}/*.json | English snapshot for machine-translation diffing (CI only) |

Community partial locales (es, uk, ta, pt, ms, zh-Hans, nb-NO, etc.) live under locales/ with no sidecars. Some folders contain only the categories object in apgames.json where volunteers translated tag labels before other game strings existed.

locale-src/ sidecars

Managed locales (de, fr, it, es-US) are machine-translated by scripts/translate.mjs when English changes on develop (Dev Server CI). The script compares each key against a flat English snapshot in locale-src/ to detect stale translations.

When you remove keys from English, develop CI prunes the matching entries from managed locales/ files and locale-src/ sidecars before translation runs (no Gemini API call). Community partial locales are not auto-pruned — they remain Weblate-managed.

One-time migration: node scripts/split-locale-src.mjs (already run; kept for reference).

Weblate merges

  1. Merge migration before accepting Weblate PRs that strip _src
  2. Close bulk "Cleanup translation files" PRs — they delete metadata and unrelated keys
  3. Small human-editing PRs merge normally; locale-src/ is never in the diff
  4. In hosted Weblate: disable the "Cleanup translation files" add-on for managed components

Hosted Weblate vs GitHub develop

GitHub origin/l10n/weblate is reset to match develop after imports (npm run merge-weblate-branch). Hosted Weblate also keeps its own git export (weblate/develop remote). Those histories diverge easily; Weblate then reports “Could not merge the repository” for many locales/* files.

Do not resolve that with git merge weblate/develop and checkout --theirs — that drops develop machine translations.

  1. git fetch origin weblate (remote weblate → https://hosted.weblate.org/git/abstract-play/gameslib/)
  2. npm run overlay-weblate-locales -- --ours origin/develop --theirs weblate/develop (pass explicit locale paths, or rely on default diff vs last merge)
  3. Commit and git push origin develop
  4. npm run reset-weblate-export — force-with-lease origin/l10n/weblate to the same commit as origin/develop (you cannot git push to hosted.weblate.org; that remote is read-only)
  5. In Weblate: lock → Repository → Update (or wlc pull) → unlock. If Diagnostics still show merge failures, use Repository maintenance → Reset (admin), then update again.

npm run check-weblate-branch compares origin/develop to origin/l10n/weblate only; hosted Weblate can still be out of date until step 5.

Weblate key naming

Weblate's i18next parser treats a bare key as conflicting with siblings whose names look like plural or context suffixes (_one, _other, _2, _11, etc.) in the same JSON object. For example, do not place INSTRUCTIONS alongside INSTRUCTIONS_one — rename the non-plural key (e.g. INSTRUCTIONS_ANY).

npm run lint runs check-weblate-keys against locales/en/*.json and fails on conflicts. Use a distinct stem for each purpose (e.g. nowhere_what vs nowhere_count_one/nowhere_count_other).

In game code

description: "apgames:descriptions.complica"

Add keys when introducing new user-visible text. Variant names in gameinfo also reference apgames.json.

Game titles (names.{uid})

Meta-game display titles use apgames:names.{uid} (e.g. apgames:names.agere → "Adere"). English values are generated from gameinfo.name in source via npm run sync-game-names-locale; CI enforces parity with npm run check-game-names-locale.

Descriptions and notes continue to use apgames:descriptions.{uid} and apgames:notes.{uid}.

Category tag labels (categories.*)

Explore filters, game pickers, and meta pages show gameinfo.categories tag ids using localized strings from locales/en/apgames.json → categories. Each tag id has tag, full, and description.

Many keys are dynamic (categories.${tagId}.tag); do not run prune tools that drop unreferenced paths.

Render area labels

Seat-specific stash and panel titles under validation.<game> should use the placeholder (not). Games emit structured labels via seatAreaLabel(); do not call i18next.t() for those labels inside render(). Front resolves keys at draw time. See Structured render labels.

Sidebar status and score table headers use apgames:status.* keys the same way: emit neutralAreaLabel() from sidebarStatuses() / sidebarScores(); front resolves in setStatus().

addResource

Front end merges bundles via APGames.addResource(lang) — see API.

Consumers (front, node-backend, node-backend crons)

Repo Display Notifications
front getGameDisplayName() → resolveGameName(uid, gameinfo.name) after APGames.addResource —
node-backend — localizedGameName() in lib/gameDisplayName.ts; initi18n preloads apgames from gameslib/locales/*
node-backend crons/ — Same pattern in starttournaments.ts for tournament emails

Pin @abstractplay/gameslib via each repo's ci-deps.dev.json / ci-deps.prod.json. Front runs npm run sync-locales after bump to copy updated apgames (including names) into public/locales/.

Scripts

Script Purpose
npm run sync-game-names-locale Regenerate names.{uid} in English from gameinfo.name
npm run seed-game-names-managed-locales Copy English names into de/fr/it/es-US + locale-src (skip MT)
npm run check-game-names-locale Fail when names diverges from registry (part of lint)
npm run translate Gemini incremental translate en → de/fr/it/es-US (names.* never MT; English fill-in only for missing keys)
npm run prune-locales Remove stale keys from managed locales + locale-src/ (no API; develop CI runs this before translate)
npm run check-locale-json-format Fail when locales/ or locale-src/ JSON is not canonical 2-space (--write to normalize)
npm run check-weblate-keys Fail on Weblate plural-stem key conflicts in English locales
npm run overlay-weblate-locales Reconcile hosted Weblate export with develop (keep MT; overlay human strings)
npm run reset-weblate-export Force origin/l10n/weblate to match origin/develop (then pull in Weblate UI)
npm run check-locale-readiness Audit locales vs English
node scripts/publish-locales.mjs --stage dev|prod Upload supported locales to S3

Example games