Add internationalisation (i18n) support to the platform UI - #8312
Add internationalisation (i18n) support to the platform UI#8312hmjvalineY wants to merge 5 commits into
Conversation
|
Pushed four more commits and rewrote the description to match. What changed since the first push: the extraction now covers the whole UI rather than just login and sign-up. The infrastructure on its own left a signed-in user reading English on every page, so setting the language preference did not visibly do anything — that felt like half a feature. 422 files, 2,879 locale keys, en and zh-TW in step. Getting there turned up a few things worth flagging, all handled:
Full build and test run on this branch: lint 0 errors (11 pre-existing warnings, unchanged), 3,346 forge tests, 886 frontend tests, 61 system tests, build clean, migration verified on SQLite. Manually verified against a database with real data. Details in the description. On the size: 422 files is a lot, and most of it is mechanical |
The platform UI was English-only with no i18n infrastructure, while the Node-RED editor embedded inside it already ships 10 locales. A user with a Chinese browser locale got a localised editor inside an English shell. Adds the plumbing, an `en` baseline for the login and sign-up pages, a `zh-TW` locale, and a per-user language preference. Frontend uses vue-i18n; the backend uses fastify-i18n over node-polyglot. Locale files live in `frontend/src/locales/<locale>.json` and `locales/<locale>/common.json`. Both webpack entrypoints register the plugin, so the first-run setup flow can be translated too. Locale resolution is: the user's stored `language`, then the request or browser locale, then `en`. The login and sign-up pages render before there is a session, so browser detection is what makes them translatable at all; signing in must not reset a user with no stored preference to English. `fastify-i18n` narrows a regional tag onto its base language but does not widen a script-qualified one, so LOCALE_ALIASES maps `zh-Hant-TW` onto `zh-TW` rather than letting it fall back to English. Existing API error strings are deliberately left untranslated. Several are part of the de-facto contract — the sign-up page and a unit test both match `'user registration not enabled'` literally — so translating them would be a breaking change. Those responses already carry a stable `code` field, which is what callers should branch on. Tests assert every locale defines the same keys as `en`, and that the frontend and backend agree on the supported set, so a missing translation fails CI rather than silently rendering English. Refs FlowFuse#8311 Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
The i18n plumbing landed with only the login and sign-up pages extracted, which meant setting the language preference changed almost nothing a signed-in user actually looks at. Extends the extraction to the surfaces that appear on every page: - frontend/src/stores/ux-navigation.js - all 33 sidebar entries and the section headings above them. Read through the active locale inside the getters, so switching language re-renders the sidebar without a reload. - frontend/src/components/PageHeader.vue - the user dropdown - frontend/src/pages/account/Settings.vue - the remaining form fields, buttons and alerts, plus theme options moved from data() to computed so they follow the locale Locale files grow from 79 to 112 keys, still in step across en and zh-TW. Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
The earlier passes covered the login and sign-up pages, the sidebar and the account settings page. This extends the extraction to the rest of the UI, so setting the language preference changes what a signed-in user actually reads on every page. Covered in this pass: - template text nodes and display attributes across 327 .vue files, including text that wraps across lines and strings inside directive expressions - display strings defined in plain JS — table column headers, dialog copy, route meta titles, prop defaults — read through a new bare `t()` export from frontend/src/i18n.js, since there is no component instance to hang `$t` off in those places - validation errors and toast messages assigned to `errors.*` or passed to `alerts.emit` - pluralised counts, moved from the ad-hoc `pluralize()` helper onto vue-i18n's own plural syntax, so a locale without plural forms reads correctly - role names, via a new utils/roleLabels.js. `RoleNames` itself is left alone — it maps roles to identifiers that code compares against, and is not display text — so the API keeps returning `roleName` and gains a separate translated `roleLabel` for the tables that show it Deliberately left in English: `alt="FlowFuse"` and other brand names, enum values such as `popup-type`, and Vue component and route `name:` properties. utils/pipelineValidation.js is also untouched — it is maintained as a 1:1 copy of forge/lib/pipelineValidation.js, which cannot import the frontend locale. Locale files grow from 112 to 2,521 keys, still in step across en and zh-TW, enforced by the existing parity test. Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
Three surfaces the earlier passes could not reach, each for a different
reason:
- Audit log descriptions in AuditEntryVerbose.vue mix literal text with
interpolation, so a plain text-node match skipped them. They now use
vue-i18n named interpolation, with the original expressions passed
through as `{p0}`, `{p1}` and so on.
- Audit event labels live in data/audit-events.json, which stays the
source of truth for which events exist and how they group. A new
`auditEvents` namespace holds the labels, keyed by the camel-cased
event id, and services/audit-events.js reads them through `t()`.
- Welcome tour bodies are HTML strings in tour-welcome.js, so the value
never looked like a plain display string.
Also picks up the instance state tiles (Running / Error / Not Running),
the audit log entry count, and the account and teams side-nav tabs.
The event-id key shape has to match between the locale files and the
runtime, and it is not the obvious slug: ids such as
`application.deviceGroup.created` already contain camel case, so the
segment after the separator keeps its own casing rather than being
lower-cased. All 165 event ids resolve.
Locale files grow to 2,803 keys across `ui` and the new `auditEvents`
namespace, still in step across en and zh-TW.
Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
A text node such as `There are no dashboards {{ scope === 'application'
? 'in this application' : 'in this team' }}.` mixes literal words with an
expression, so the earlier passes - which matched whole text nodes -
skipped it. These now use vue-i18n named interpolation, with the original
expressions passed through as {p0}, {p1} and so on.
Also translates the English string literals that some of those
expressions produced, and adds keys for the surrounding words. Two
literals are deliberately left: 'application' is a scope comparison and
'main' is a git branch default, neither is display text.
Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
Adds internationalisation to the platform UI: the plumbing, full
enandzh-TWlocales, and a per-user language preference.Written fresh against
mainrather than rebased fromfeature/i18n-implementation, which is now ~4,700 commits behind and pinnedfastify-i18n@^1(Fastify 4 era) andvue-i18n@^9. The architecture in that branch'sdocs/contribute/i18n.mdis what this follows — library choices, locale directory layout, hierarchical key naming — with the code written against current APIs.Related Issue(s)
#8311
Scope
This grew well beyond the two pages the issue proposed. The infrastructure alone left a signed-in user reading English on every page, so the extraction was carried through the whole UI. It is now 5 commits, and I am happy to split it if that is easier to review — see the note at the end.
zh-TWlocale,User.languagepreference, login and sign-up pagesLocale files: 2,879 keys, en and zh-TW in step.
What's here
Libraries —
vue-i18n@^11on the frontend,fastify-i18n@^3(overnode-polyglot) on the server. Their interpolation syntaxes differ ({name}vs%{name}); that is documented rather than papered over.Locale files —
frontend/src/locales/<locale>.jsonandlocales/<locale>/common.json.Both webpack entrypoints.
webpack.config.jsbuilds two separate Vue apps,mainandsetup. The plugin is registered on both, so the first-run setup flow is translated too.Locale resolution — the user's stored
language, then the request or browser locale, thenen. The login and sign-up pages render before there is a session, so browser detection is what makes them translatable at all. A user with no stored preference keeps whatever the browser negotiated; signing in does not reset them to English.LOCALE_ALIASES.fastify-i18nnarrows a regional tag onto its base language (en-GBfindsen) but does not widen a script-qualified tag onto a regional one, sozh-Hant-TWwould fall back to English rather than findingzh-TW. Chrome reports exactly that tag for Traditional Chinese on some platforms.forge/i18n/locales.jsmaps it. I had assumed the library handled this; a test proved otherwise, which is why the alias map exists.Strings outside components. Table column headers, dialog copy, route meta titles and prop defaults have no component instance to hang
$toff, sofrontend/src/i18n.jsexports a baret()for them.Pluralisation. The ad-hoc
pluralize()helper produced1 Device/2 Devicesby appendings, which cannot work for a locale without plural forms. Those 15 call sites now use vue-i18n's own plural syntax, soenstill reads correctly andzh-TWdoes too.Audit log.
data/audit-events.jsonstays the source of truth for which events exist and how they group; a newauditEventsnamespace holds the labels, keyed by the camel-cased event id. All 165 event ids resolve. The description sentences use named interpolation.Locale parity tests.
test/unit/forge/i18n/locales_spec.jsasserts every locale defines the same keys asen, and thatfrontend/src/i18n.jsandforge/i18n/locales.jsagree on the supported set. A missing translation fails CI rather than silently rendering English. This mirrors whatscripts/lint-colors.jsalready does for theme files.Deliberate scope decisions
API error strings are not translated. Several are part of the de-facto contract:
frontend/src/pages/account/Create.vuebranches onerr.response.data.error === 'user registration not enabled', andtest/unit/forge/routes/auth/index_spec.jsmatches the same literal. Translating them would break both. Those responses already carry a stablecodefield (user_registration_unavailable), which is what callers should branch on — migrating consumers to it, then translating the human-readable text, is separate work. The server plumbing is registered and tested so that work does not start from nothing; the natural first consumer isforge/postoffice/templates/.RoleNamesis left alone. It maps roles to identifiers that code compares against and that feed table search, so it is not display text. The API keeps returningroleNameand gains a separate translatedroleLabelfor the places a role is shown. A unit test caught this when I first changedroleNameitself.utils/pipelineValidation.jsis untouched. Its header says it is maintained as a 1:1 copy offorge/lib/pipelineValidation.js, which cannot import the frontend locale.Also left in English on purpose: Vue component and route
name:properties (identifiers, not labels),alt="FlowFuse"and other brand names, enum values such aspopup-type, theEnglishlabel inSUPPORTED_LOCALES(deliberately in its own language), and content that comes from the database — team type names, instance types, stack labels.User.languageis stored server-side, unlike the theme preference which is local-only, because content generated outside a browser session needs it — most obviously the emails inforge/postoffice/templates. Happy to be redirected if you would rather this lived elsewhere;Userhas no generic preferences mechanism today, so this adds a flat column.Build and test results
Environment: Windows 11, Node v24.14.0, npm 11.9.0, SQLite.
Rebased onto
mainate2ef8dfc4(#8202, agoogle-auth-librarybump) and re-run in full at48afd0a2e. Both sides had touchedpackage.jsonandpackage-lock.json, so the merged lock was verified withnpm install --package-lock-only, which reported nothing to change — declared and resolved versions agree for the bumped dependency and for both i18n libraries. Every result below is from after the rebase, not carried over from before it.npm run linteslint-disabledirectives in test files)npm run lint:colorsnpm run test:unit:forgenpm run test:unit:frontendnpm run test:systemnpm run buildmainandsetupentrypoints emitted; same 2 pre-existing asset-size warningsnpm run test:docsdocs/contribute/i18n.md: 4 links tested, 4 valid, 0 errors. To be precise about this one: the suite as a whole exits non-zero on 342 errors in files this PR does not touch, a good number of them the checker resolving../../above the repo root on Windows. This PR adds one docs file and modifies none, so it neither causes nor fixes any of them.One disclosure on the frontend suite: one run out of five failed two timing-sensitive tests while the forge suite was running concurrently on the same machine (that run's transform time was 20s against a typical 12s). Suites were run one at a time after the rebase and all are clean. Not something this change introduces, but worth stating rather than only reporting the green runs.
Bundle cost:
main4.90 → 5.29 MiB,setup4.86 → 5.19 MiB — the locale files ship in both entry bundles.Migration, on SQLite:
MetaVersionsas the latest entryPRAGMA table_info('Users')confirmslanguage VARCHAR(255), nullable, default NULLcheck-migrations.yml—20260827-01sorts last in the directoryManual verification, browser locale
zh-TW, against a database carrying real data (5 users, a team, a running Node-RED instance):navigator.languagealone;<html lang>is set correctlyenrenders English — both directions work<i18n-t>renders sentences with a link inside them in the right word orderPUT /api/v1/userwithzh-TW→ 200 and persists;fr-FRandnonsense→ 400 from the schemaenum;null→ 200 and clears the preferencelocalStoragedeliberately set toen, a reload applies the storedzh-TWpreference instead — precedence is correctChecklist
docs/contribute/i18n.mdflowforge.ymlchangesflowforge.yml? — noFlowFuse/helmto update ConfigMap Template — n/aFlowFuse/CloudProjectto update values for Staging/Production — n/aLabels
area:migrationlabel (I cannot add labels as an outside contributor)Notes for reviewers
On the size. 422 files is a lot to review at once, and most of it is mechanical
$t()substitution plus two locale files. If it helps, commit 1 stands alone as the infrastructure and is the part worth real scrutiny; commits 2–5 are the extraction. I can split this into separate PRs, or drop thezh-TWvalues and land only the plumbing plus theenextraction, whichever you prefer — say the word.On which pages. The issue proposed starting with login and sign-up. I went further because the preference does not visibly do anything until the pages a signed-in user actually reads are covered. If you would rather review a smaller surface first, I can trim.
I run FlowFuse self-hosted for a Traditional Chinese speaking team, so I have a reason to keep the
zh-TWlocale current rather than land it and disappear.