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:

Language resolution order:

  1. Manual choice in the footer (stored in localStorage as i18nextLng)
  2. Browser / system language
  3. 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 PR workflow

  1. Close bulk "Cleanup translation files" PRs (e.g. ones that delete _src or prune unrelated keys)
  2. Merge small human-translation PRs normally after the locale-src migration is on develop
  3. Disable the Weblate "Cleanup translation files" add-on for the frontend component

Related