|
| 1 | +# Internationalisation |
| 2 | + |
| 3 | +DataLab-Web ships a working internationalisation framework. English is the **source language** (the `msgid`) and French is the first translated locale. The UI auto-detects the user's regional preference and renders fully in their language, including the labels that come from Python (Sigima / guidata) through the Pyodide bridge. |
| 4 | + |
| 5 | +## How it works for users |
| 6 | + |
| 7 | +The active locale is resolved **once per page load** in this order (first match wins): |
| 8 | + |
| 9 | +1. `?lang=<code>` URL query parameter (handy for sharing links and E2E tests); |
| 10 | +2. an explicit choice persisted in `localStorage["datalab-web:lang"]` (set via the language selector in the menu bar); |
| 11 | +3. the browser's regional preference (`navigator.languages`); |
| 12 | +4. English (`DEFAULT_LOCALE`) as the fallback. |
| 13 | + |
| 14 | +Switching language from the menu-bar selector persists the choice and triggers a **full page reload**, because the Pyodide instance pins its `LANG` at boot and Sigima/guidata cache their gettext labels at import time — so a fresh instance must boot with the new locale. |
| 15 | + |
| 16 | +## Architecture |
| 17 | + |
| 18 | +There are two translation surfaces, kept in sync: |
| 19 | + |
| 20 | +- **TypeScript/React strings** go through a lightweight `t()` helper ([`src/i18n/translate.ts`](../src/i18n/translate.ts)). The English string _is_ the key; for English the helper is the identity function (no `en.json` is shipped), and `{name}` placeholders are interpolated from a `vars` argument. Catalogs live in [`src/locales/<code>.json`](../src/locales/) and are merged in [`src/i18n/catalogs.ts`](../src/i18n/catalogs.ts). React components read the locale via `useTranslation()` from [`src/i18n/I18nProvider.tsx`](../src/i18n/I18nProvider.tsx); non-React code (the action registry, `runtime.ts`) reads it directly from [`src/i18n/locale.ts`](../src/i18n/locale.ts). |
| 21 | +- **Python-origin labels** (signal/image creation types, processing/operations/analysis menu entries, parameter-dialog field labels) are translated by Sigima/guidata's own gettext `.mo` catalogs. The active locale is mapped to a `LANG` value by `pyodideLang()` (English → POSIX `C`, i.e. untranslated `msgid`; any other locale → its bare code such as `fr`) and exported into the Pyodide environment **before** the first guidata/sigima import in [`src/runtime/runtime.ts`](../src/runtime/runtime.ts), [`src/runtime/macroWorker.ts`](../src/runtime/macroWorker.ts) and [`src/runtime/notebookWorker.ts`](../src/runtime/notebookWorker.ts). |
| 22 | + |
| 23 | +Applying `t()` to a Python-origin label is **safe**: a label that Sigima already owns in French has no key in `fr.json`, so `t()` returns it unchanged; only the English override strings defined in [`src/runtime/processor.py`](../src/runtime/processor.py) and the React-owned strings get looked up. |
| 24 | + |
| 25 | +## Contributing translations (developer workflow) |
| 26 | + |
| 27 | +1. **Wrap every new user-facing string** in `t("…")` (import from `src/i18n/translate`). Use `t("Delete {count} objects?", { count })` for interpolation. Do **not** translate brand names (e.g. _DataLab Web_) or AI-assistant system prompts. |
| 28 | +2. **Extract the keys.** Run `npm run i18n:extract` to merge newly discovered `t("…")` keys into `src/locales/fr.json` (existing translations are preserved; new keys get empty placeholders). Keys referenced only through a variable (never as a string literal) must be listed in [`src/locales/_dynamic-keys.json`](../src/locales/_dynamic-keys.json) so the extractor seeds them. |
| 29 | +3. **Fill in the translations** in `src/locales/fr.json`. |
| 30 | +4. **Verify** with `npm run i18n:check` (fails on missing or empty keys; this also runs the static scan). It is wise to run it in CI. |
| 31 | +5. **Add a new locale** by: adding its code to `SUPPORTED_LOCALES` and `LOCALE_LABELS` in [`src/i18n/locale.ts`](../src/i18n/locale.ts), registering its catalog in [`src/i18n/catalogs.ts`](../src/i18n/catalogs.ts), adding the code to `LOCALES` in [`scripts/i18n-extract.mjs`](../scripts/i18n-extract.mjs), and creating `src/locales/<code>.json`. Python-side labels are picked up automatically if the matching `.mo` catalogs are bundled in the Sigima/guidata wheels. |
| 32 | + |
| 33 | +Coverage is enforced by Vitest unit tests ([`tests/ts/i18n/i18n.test.ts`](../tests/ts/i18n/i18n.test.ts)) and an end-to-end Playwright spec ([`tests/e2e/i18n.spec.ts`](../tests/e2e/i18n.spec.ts)) that boots the app with `?lang=fr` and asserts both a translated UI menu and a French Sigima label coming through the Pyodide bridge. |
0 commit comments