Ctrl K
ComunidadGuías…

docs-for-dev/I18N.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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.