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.
- Weblate syncs only
locales/— neverlocale-src/ - Develop CI updates both
locales/andlocale-src/on auto-translate commits — iflocale-src/is still dirty after translate, the auto-commit step failed and the next run will retranslate the same keys - Production CI (
main) does not run prune or Gemini translate; it publishes whatever locale JSON is already on the branch (typically merged fromdevelopand Weblate) - S3 publish uploads
locales/directly (no_srcstripping needed)
One-time migration: node scripts/split-locale-src.mjs (already run; kept for reference).
Weblate merges
- Merge migration before accepting Weblate PRs that strip
_src - Close bulk "Cleanup translation files" PRs — they delete metadata and unrelated keys
- Small human-editing PRs merge normally;
locale-src/is never in the diff - 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.
git fetch origin weblate(remoteweblate→https://hosted.weblate.org/git/abstract-play/gameslib/)npm run overlay-weblate-locales -- --ours origin/develop --theirs weblate/develop(pass explicit locale paths, or rely on default diff vs last merge)- Commit and
git push origin develop npm run reset-weblate-export— force-with-leaseorigin/l10n/weblateto the same commit asorigin/develop(you cannotgit pushtohosted.weblate.org; that remote is read-only)- 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.
gameinfo.namein each game file stays the English canonical string (export snapshot, i18n fallback).resolveGameName(uid, englishFallback?)resolves the localized title from the active i18next locale; callers passgameinfo.nameas fallback when the key is missing. Exported from the package root (index.js/index-browser.js), notpublic-api— browser builds must not pull ini18n-node(fs).- Run
npm run sync-game-names-localewhen adding a game or renaminggameinfo.name. - Managed locales (
de,fr,it,es-US) default to English fornames(most titles are proper nouns).scripts/translate.mjsnever machine-translatesnames.*: it copies English only for missing keys and leaves existing locale values (including human Weblate edits) unchanged. Runnpm run seed-game-names-managed-localesafter updating Englishnamesto batch-seed new keys into locales +locale-src.
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.
- Add or update all three when introducing a new tag id (same PR as the
gameinfochange). - Managed locales:
categories.*entries live inlocale-src/*/apgames.jsonand are machine-translated like otherapgameskeys. - Tag semantics and catalog lists: Categories & tags.
- Front consumes the
apgamesnamespace only for these keys (notapfront).
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
- Complica — typical
apgames:key usage