Internationalization
The client uses i18next with react-i18next for translatable UI strings.
Key files
| File | Role |
|---|---|
src/i18n.js |
i18next initialization and supported language list |
src/index.js |
Imports ./i18n before render |
src/components/LanguagePicker.js |
Footer language dropdown |
public/locales/ |
Translation JSON for HTTP loading (fr/de/it and S3 publish) |
src/locales/en/ |
Bundled English fallback (synced at build time; gitignored) |
locale-src/ |
English snapshots for CI machine translation (de/fr/it/es-US only) |
Configuration
From i18n.js:
- Namespaces:
apfront,apgames,apresults - Fallback language:
en - Supported languages:
en,fr,de,it - English: bundled in the JS artifact via
resources.en(partialBundledLanguages: true) - Other UI languages:
i18next-http-backendloads from/locales//.json - Detector:
i18next-browser-languagedetectorwithlocalStoragebeforenavigator
Language resolution order:
- Manual choice in the footer (stored in
localStorageasi18nextLng) - Browser / system language
- English fallback (including unsupported locales and failed HTTP loads)
Hybrid loading model
| Namespace | English | fr / de / it |
|---|---|---|
apfront |
Bundled in JS | HTTP from S3 |
apgames |
Bundled in JS (synced from gameslib) | HTTP from S3 |
apresults |
Bundled in JS (synced from gameslib) | HTTP from S3 |
Author English strings in public/locales/en/apfront.json. npm run sync-locales (runs before start and build) copies gameslib namespaces into public/locales/ and mirrors all three English namespaces into src/locales/en/ for webpack bundling. English updates require an app deploy; non-English locale updates can ship via bin/publish-locales.mjs alone.
Production builds delete build/locales from the SPA artifact; locale JSON is published separately to S3. Bundled English ensures the UI still works when /locales/en/* is unavailable.
Usage in components
import { useTranslation } from "react-i18next";
function MyComponent() {
const { t } = useTranslation();
return <span>{t("some.key")}</span>;
}
Skeleton wraps the app in Suspense because useTranslation can suspend while loading locale files for non-English languages.
Extracting strings (optional)
npm run extract
Runs i18next-parser over src/**/*.{js,jsx} (configured in package.json), writes to public/locales/, and copies English apfront into src/locales/en/ for bundling. Review generated keys in public/locales/ before committing.
Locale support
Four UI locales: English, French, German, and Italian. Users change language via the footer dropdown. Relative timestamps (javascript-time-ago) follow the active locale via TimeAgoLocaleSync.
UI language vs communication language
These are independent:
| Setting | Where | Stored | Used for |
|---|---|---|---|
| UI language | Footer LanguagePicker |
Browser localStorage (i18nextLng) |
Website strings (apfront namespace) |
| Communication language | User settings → UserSettingsModal |
user.language on backend (new_setting) |
Emails and push notifications (apback on node-backend) |
Changing communication language does not call i18n.changeLanguage or alter footer/localStorage UI preferences.
locale-src/ sidecars
Managed UI locales (de, fr, it, es-US apfront.json) store English source-tracking metadata in locale-src/ at the repo root, not inside public/locales/. This keeps Weblate PRs from deleting _src blocks that bin/translate.mjs needs.
- Weblate edits
public/locales/only; human edits are authoritative when English is unchanged - CI (
bin/translate.mjson develop deploy only) machine-translates new/changed English and maintainslocale-src/stamps; main publishes locales from the merged develop tree without calling Gemini - Games namespaces (
apgames,apresults) are synced from gameslib vianpm run sync-locales
Weblate PR workflow
- Close bulk "Cleanup translation files" PRs (e.g. ones that delete
_srcor prune unrelated keys) - Merge small human-translation PRs normally after the locale-src migration is on
develop - Disable the Weblate "Cleanup translation files" add-on for the frontend component