Translations¶
The web UI is translated into 13 languages besides English (spec 023). This page is for contributors: how text gets into the UI, how the language files are made and checked, and how the device gets them.
How it fits together¶
flowchart LR
accTitle: How a translation reaches the device
accDescr: The UI's English strings and the firmware's messages go into en.json. The translation tool turns new and changed keys into a file per language. The release job builds a gzip file and checksum for each language. The device downloads its language's file over HTTPS into its storage, and the web UI loads it from the device before it starts.
src["web/src (t() calls)"] --> en["web/src/i18n/en.json<br/>+ en.context.json"]
fw["firmware messages<br/>(lib/, src/)"] -->|gen_device_catalog.py| en
en -->|translate.py + review| loc["web/src/i18n/locales/<code>.json"]
loc -->|build_packs.py in the release job| rel["release assets<br/>sqmeter-i18n-<code>.json.gz + .sha256"]
rel -->|HTTPS download| dev["device LittleFS<br/>/lang.json.gz"]
dev -->|GET /lang.json| ui["web UI"]
Diagram in words
- The UI's `t()` strings and the firmware's messages (through `gen_device_catalog.py`) make up `web/src/i18n/en.json`, with a context note per key. - `translate.py` (with a review pass) turns new and changed keys into `web/src/i18n/locales/.json`.
- The release job's `build_packs.py` makes `sqmeter-i18n-.json.gz` and its `.sha256` for each language.
- The device downloads its language's file over HTTPS into LittleFS (`/lang.json.gz`), and the web UI loads it from `/lang.json` before it starts.
- English is built in:
web/src/i18n/en.jsonis bundled with the UI and is the fallback for every key. - Other languages are files: a release publishes
sqmeter-i18n-<code>.json.gz(gzip of{lang, version, messages}, at most 64 KB) with a.sha256sidecar ("<hash> <size>") andsqmeter-i18n-manifest.json. - The device downloads one file: when
languagechanges,src/LanguagePackfetches the sidecar and the file over the same TLS path as firmware updates (never during one), checks size, gzip header and SHA-256, and renames it into place. English deletes it. Manual upload:POST /api/i18n/upload. See the REST API. - The UI loads it before the app:
web/src/main.tsxasks/api/i18n, fetches/lang.json, then imports the app. A missing or damaged file, or one from another version, leaves English (per key) and a notice. - Device text (settings errors, safety reasons, alert history, API errors) stays English on the wire.
tools/i18n/gen_device_catalog.pyturns each firmware message into adevice.*template inen.json; the UI matches device text against the templates (deviceText(),deviceError()) and shows the translation.
Writing UI text¶
- Use
t('area.key', { name: value })fromweb/src/i18n. Placeholders are{name}; plurals are objects keyed by CLDR category ({"one": "{count} channel", "other": "{count} channels"}), filled with{count}. - Never build a sentence from pieces (
a + ' and ' + b,`${n} samples`). Use one key with placeholders,Intl.ListFormatfor lists, andweb/src/i18n/format.tsfor numbers, times and durations. - Don't call
t()when a module loads (rule I18N-04). - Add a context note for every new key in
web/src/i18n/en.context.json: where it appears and any length limit (max 16 chars).python3 tools/i18n/context.pyadds a generated note you can improve. - Layout: logical CSS properties only, so Arabic mirrors (I18N-03).
The rules are I18N-01..04 in the coding standard.
Adding or changing a string¶
- Change the English in
en.json(and the note inen.context.json). -
Translate the new and changed keys:
python3 tools/i18n/translate.py --all --dry-run # what changed since the last translation ANTHROPIC_API_KEY=... python3 tools/i18n/translate.py --allFor each language it sends only the keys whose English changed (tracked in
tools/i18n/record.json), with their context notes, the glossary (tools/i18n/glossary/<code>.json: terms, the names to keep, the register) and the plural forms. A second call reviews the draft as a native UI editor and back-translates the safety strings. The result is checked withcheck.mjsand written only if it passes; the review notes are appended totools/i18n/review/<code>.md. -
If you edit a translation by hand instead, mark it current:
python3 tools/i18n/translate.py --lang es --record.
Checks¶
| Check | What it does | Where |
|---|---|---|
node tools/i18n/literals.mjs |
I18N-01: no hard-coded UI text | quality gate, build |
node tools/i18n/check.mjs |
completeness, extra keys, placeholders, plural forms, edge spaces, context notes, glossaries (--review adds a worklist: glossary misses, untranslated text, length) |
quality gate (I18N-02), build |
gen_device_catalog.py --check, context.py --check |
generated files are current | quality gate, build |
python3 -m unittest discover -s tools/i18n |
the translation tool and the file builder | build |
build_packs.py |
every file builds under 64 KB | build, release |
web/tests/i18n.spec.ts |
every page in every language at 320 px and 1280 px: lang/dir, no sideways scroll, screenshots; Arabic with the axe checks and left-to-right readings |
Deploy Docs & Demo |
Run the Playwright check for some languages only: I18N_LANGS=ar,de npx playwright test tests/i18n.spec.ts. The demo opens in a language with ?lang=<code>.
Adding a language¶
- Add it to
web/src/i18n/languages.tsandlib/LanguageLogic(the device validates the code). - Write
tools/i18n/glossary/<code>.json(terms, register, notes). - Run
translate.py --lang <code>, then the review:node tools/i18n/check.mjs --reviewand a note intools/i18n/review/<code>.mdwith the safety strings back-translated. - Run the Playwright check for the language.