# Interface and reply languages DeepTutor has two language settings that are deliberately separate: - **Interface language** (`language`) selects the web UI locale: navigation, settings, status text, deterministic backend copy and the CLI. - **Model output language** (`response_language`) tells chat and the learning capabilities which language to write reader-facing answers in. It supports more languages than the interface. A language can be one, the other, or both. Adding a locale to the web picker does not make the model answer in it, and adding a reply language does not translate the UI. ## Where the copy lives | Surface | Source | Fallback | | --- | --- | --- | | Web UI | `web/locales//app.json` (and `common.json` when present) | English, per missing key | | Web language picker | `web/i18n/languages.ts` | — | | Web date and time formatting | `web/lib/datetime.ts` | English (`en-US`) | | Desktop shell | `apps/desktop/src/i18n.mjs` | English, per missing key | | Reply-language picker | `web/features/settings/store/response-language-options.ts` (`RESPONSE_LANGUAGE_OPTIONS`) | — | | Runtime normalizer | `deeptutor/services/settings/interface_settings.py` | English | | Reply-language contract | `deeptutor/core/response_languages.py` | English | | Prompt language directive | `deeptutor/services/prompt/language.py` | English | | CLI banner and `deeptutor init` | `deeptutor/runtime/banner.py` | English | | Backend API/MCP/sandbox messages | `deeptutor/services/i18n.py` | English | | Book Overview page | `deeptutor/book/overview_copy.py` | English | | Book Markdown export | `deeptutor/book/export.py` | English | | Quiz judge | `deeptutor/api/routers/quiz_judge.py` | English | | Partner "thinking…" notice | `deeptutor/services/partners/runtime.py` | English | Most web catalog keys are English copy; named keys such as `language.english` are also used. The runtime keeps `keySeparator: false`, so natural-language keys such as `Generating...` are looked up whole. A missing key falls back to English; a first-class locale is expected to have **no missing keys**. `web/scripts/compact-locale-loader.cjs` removes entries whose key equals their value at build time. Keep the authored JSON complete anyway: the Python locale tests and the audit read the JSON files, not the compacted bundle. ## Adding an interface language Use the existing French, German, Ukrainian and Polish locales as references. A complete addition touches every row below; skipping one produces a language that silently serves English on that surface. 1. **Web locale registration** - Add `{ code, labelKey }` to `APP_LANGUAGES` in `web/i18n/languages.ts`. - Add the code to `normalizeLanguage()` so `fr-FR`, `fr_BE`, etc. resolve. - Load the bundle in `ensureLanguage()` in `web/i18n/init.ts`. - Add `language.` to every catalog. - Register the appropriate regional locale in `getLocale()` in `web/lib/datetime.ts`; test actual date and time formatting with an explicit time zone so the test does not depend on the host's location. 2. **Web catalog** - Create `web/locales//app.json`. If the English tree ships a `common.json` for the same namespace, mirror that file too. - Cover every key in the English catalogs, and keep every `{{placeholder}}` byte-identical, including repeated occurrences. - Keep plural suffixes expected by i18next for that locale (`_one`, `_other`, and `_many` where the language uses it). - Do not translate product names, model ids, provider names, code identifiers or literal placeholders such as `https://`. - Do not reorder the existing English keys just to translate them; a focused diff is easier to review. 3. **Backend interface language** - Add the code to `UiLanguage` and `_normalize_language()` in `deeptutor/services/settings/interface_settings.py`. - Add it to `_normalize_language()` in `deeptutor/services/config/launch_settings.py` so CLI/startup language overrides survive normalization. - Add it to `_LANGUAGE_CHOICES` in `deeptutor/services/config/settings_spec.py`. 4. **Reply language, if the language is also offered for answers** - Add it to `SUPPORTED_RESPONSE_LANGUAGES` in `deeptutor/core/response_languages.py`. - Add it to `ResponseLanguage` and `isResponseLanguage()` support in the shared `web/i18n/languages.ts` / `web/context/app-shell-storage.ts` contract and to `RESPONSE_LANGUAGE_OPTIONS` in `web/features/settings/store/response-language-options.ts`. - Add its label to `_LANGUAGE_LABELS` in `deeptutor/services/prompt/language.py`. - The shared partner reply-language picker reads `RESPONSE_LANGUAGE_OPTIONS`, so no separate partner list is needed. 5. **CLI and init wizard** - Add the code to `_pick_language()` in `deeptutor/runtime/banner.py`. - Add a `LABELS[code]` entry with exactly the same keys as `LABELS["en"]`. 6. **Deterministic backend copy** - Add `_MESSAGES[code]` and recognition in `_parse_language()` to `deeptutor/services/i18n.py`. - Add `_COPY[code]` to `deeptutor/book/overview_copy.py`. - Add `_LABELS[code]` to `deeptutor/book/export.py`. - Add a judge prompt to `_JUDGE_SYSTEM_PROMPTS` in `deeptutor/api/routers/quiz_judge.py` if the judge should answer in that language. - Add a `_THINKING_NOTICES[code]` entry in `deeptutor/services/partners/runtime.py` and preserve the language through `PartnerRunner._language()`; otherwise the notice and turn context can silently fall back to English. - For explicit book-language requests, add the corresponding cues in `deeptutor/book/language.py`. If the voice service offers the language, register it in `deeptutor/services/voice/options.py` too. 7. **Desktop shell** - Add a complete dictionary and register it in `MESSAGES` in `apps/desktop/src/i18n.mjs`. Preserve interpolation fields and intentional line breaks, and verify language-region aliases through `resolveLanguage()`. 8. **Generated contracts** - When changing a schema or language union exposed by the API, run `python scripts/export_frontend_contracts.py`, then `npm --prefix web run contracts:generate` and commit the generated changes. - After editing router source, run `python scripts/build_router_index.py`. 9. **Tests** - Add a locale test under `tests/i18n/` that checks catalog coverage, placeholder parity, the language normalizer, the CLI banner, the backend message table and any surface you touched. - Run `npm run i18n:check` from `web/`; it checks English/Chinese parity and audits used-key coverage. - Keep locale-registry expectations and the synthetic locale fixtures in `web/tests/i18n-audit.spec.ts` in sync; include each locale's plural forms. - Run `npm run typecheck`, `npm run test:node`, `npm run lint`, and the focused Vitest files. - Run all Python locale tests, not only the new language's file: adding English keys or language labels can break an existing locale's coverage. A first-class locale must cover every English catalog key, not merely the 65 % audit floor. Keep each locale's existing terminology consistent. - Run the desktop shell language tests when adding shell copy. ## Rules that keep languages from half-breaking - **No inline `{ en, zh }` maps and no `zh ? "..." : "..."` branches in components.** Use `t()` with the English copy as the key so every locale can translate it. Issue #2134 tracks the remaining instances. - **Do not add a language to a picker before its catalog and backend seams exist.** The picker will accept it and the app will quietly answer in English. - **Keep interface and reply languages aligned when the product intent is the same.** The settings page stores them separately; a user who picks French in both places expects both to be French. - **Partner reply language left empty means "Auto": follow the account's `response_language`.** An explicit partner language wins. Use `PartnerReplyLanguageSelect` rather than hand-writing the option list. - **A missing translation is not an error at runtime**, but it is a regression for a first-class locale. The fallback keeps the app usable; the tests keep the gap visible. - **Provider-returned error bodies are not translated.** They are shown as the provider wrote them; map only DeepTutor-authored copy. ## Verification From `web/`: ```bash npm run i18n:check npm run typecheck npx vitest run tests/partial-locale-fallback.spec.ts \ tests/i18n-audit.spec.ts tests/provider-branding-locale.spec.tsx npm run test:node ``` From the repository root: ```bash pytest -q tests/i18n tests/book/test_overview_copy.py node --test apps/desktop/test/i18n.test.mjs ``` Add adjacent tests for any API, partner, book or judge surface you changed. A locale PR should state which surfaces were checked and which were not; the catalog test alone does not prove the CLI, book export or quiz judge.