Skip to content

Internationalisation

AdaptiveLearner runs in 8 languages. Five are first-class (DE, EN, ES, FR, EL) with native translations. Three (PT, TR, JA) ship as English passthroughs — they have all the keys but the values are still English, waiting for native speakers to contribute translations.

Where strings live

Backend

YAML catalogs in backend/config/i18n/{lang}.yaml — one file per language. The reference catalog is EN; every other catalog must have the same key tree.

# backend/config/i18n/en.yaml
common:
  save: Save
  cancel: Cancel
settings:
  title: Settings
  section_language: Language
  ...

The backend exposes GET /api/i18n/{lang} which returns the whole catalog as JSON. The frontend's useI18n hook calls it at first paint and caches the result.

Frontend fallbacks

frontend/src/i18n/fallbacks.ts carries an inline subset of strings hard-coded in the frontend bundle. These show on first paint before the backend catalog has loaded — and as a permanent fallback if the catalog endpoint returns 5xx.

The fallback resolution chain in useI18n:

  1. Backend catalog (live strings from /api/i18n/{lang}). Walk dot-notation path.
  2. Hardcoded frontend fallbacks (first-paint resilience).
  3. Caller-supplied fallback string.
  4. The key itself.

In-app help / docs site

docs/help/_meta.yaml declares the navigation tree with DE + EN titles per entry. The in-app help panel reads it directly; scripts/generate_mkdocs_nav.py regenerates the mkdocs.yml nav block from the same source.

Help-page Markdown lives in docs/help/{lang}/... — one folder per language. mkdocs-static-i18n's docs_structure: folder mode handles the resolution.

Adding a new language

  1. Backend catalog: copy backend/config/i18n/en.yaml to backend/config/i18n/{newlang}.yaml. Translate every value (or leave as English placeholders for a passthrough).
  2. Frontend constants: add the language code to SUPPORTED_LANGUAGES in frontend/src/lib/constants.ts.
  3. Frontend fallbacks: add a {newlang}: {...} block to frontend/src/i18n/fallbacks.ts with the small subset of strings the first-paint code path uses.
  4. Assessment questions: edit plugins/adaptive-learner-plugin-assessment/adaptive_learner_assessment/questions.py — add a text_{newlang} field to every QUESTIONS entry and every answer. Re-export the JSON: poetry run python plugins/.../questions.py --export-json frontend/src/data/assessment-questions.json (no such CLI exists yet; the script in scripts/ would need a small addition).
  5. Session prompts: edit plugins/adaptive-learner-plugin-session/adaptive_learner_session/prompts.py to add a third language key to every (method, step) cell. Re-export the JSON the same way.
  6. Docs site: update mkdocs.yml's plugins.i18n.languages block and add docs/help/{newlang}/ directory mirroring the EN structure.

Parity tests

backend/tests/test_i18n_parity.py walks every catalog and asserts that:

  • Every language has every key the reference (EN) has.
  • No language has an extra key the reference doesn't have.

This catches the most common drift bug: someone adds a key in EN and forgets to add it in the other 7 files. Run make test-backend after any catalog change.

Plurals

The current catalogs don't use ICU plural syntax. The few counts we render (session count, streak days) get hard-coded form selection in TS:

const label = count === 1 ? t("session_single") : t("session_plural");

ICU plural strings via mkdocs-i18n or a runtime ICU library is on the deferred list.

RTL languages

Arabic, Hebrew, Persian etc. would need bidirectional layout support in the CSS. The current theme assumes LTR; adding RTL is on the deferred list.

When NOT to add a string

If a string only appears in developer-facing surfaces (error messages thrown by AdaptiveLearnerError, log lines, test fixture data), leave it English. The i18n machinery is for end-user-visible strings only.

The frontend's console.warn and console.error calls also stay English — they're for developers reading the browser console, not end users.