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.
The active locale is resolved once per page load in this order (first match wins):
?lang=<code>URL query parameter (handy for sharing links and E2E tests);- an explicit choice persisted in
localStorage["datalab-web:lang"](set via the language selector in the menu bar); - the browser's regional preference (
navigator.languages); - English (
DEFAULT_LOCALE) as the fallback.
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.
There are two translation surfaces, kept in sync:
- TypeScript/React strings go through a lightweight
t()helper (src/i18n/translate.ts). The English string is the key; for English the helper is the identity function (noen.jsonis shipped), and{name}placeholders are interpolated from avarsargument. Catalogs live insrc/locales/<code>.jsonand are merged insrc/i18n/catalogs.ts. React components read the locale viauseTranslation()fromsrc/i18n/I18nProvider.tsx; non-React code (the action registry,runtime.ts) reads it directly fromsrc/i18n/locale.ts. - Python-origin labels (signal/image creation types, processing/operations/analysis menu entries, parameter-dialog field labels) are translated by Sigima/guidata's own gettext
.mocatalogs. The active locale is mapped to aLANGvalue bypyodideLang()(English → POSIXC, i.e. untranslatedmsgid; any other locale → its bare code such asfr) and exported into the Pyodide environment before the first guidata/sigima import insrc/runtime/runtime.ts,src/runtime/macroWorker.tsandsrc/runtime/notebookWorker.ts.
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 and the React-owned strings get looked up.
- Wrap every new user-facing string in
t("…")(import fromsrc/i18n/translate). Uset("Delete {count} objects?", { count })for interpolation. Do not translate brand names (e.g. DataLab Web) or AI-assistant system prompts. - Extract the keys. Run
npm run i18n:extractto merge newly discoveredt("…")keys intosrc/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 insrc/locales/_dynamic-keys.jsonso the extractor seeds them. - Fill in the translations in
src/locales/fr.json. - 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. - Add a new locale by: adding its code to
SUPPORTED_LOCALESandLOCALE_LABELSinsrc/i18n/locale.ts, registering its catalog insrc/i18n/catalogs.ts, adding the code toLOCALESinscripts/i18n-extract.mjs, and creatingsrc/locales/<code>.json. Python-side labels are picked up automatically if the matching.mocatalogs are bundled in the Sigima/guidata wheels.
Coverage is enforced by Vitest unit tests (tests/ts/i18n/i18n.test.ts) and an end-to-end Playwright spec (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.