docs-for-dev/I18N.md
Rama main · réplica de lectura del repositorio original.
# 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/<code>/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.<name>` 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/<code>/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.