Skip to content

Add internationalisation (i18n) support to the platform UI - #8312

Open
hmjvalineY wants to merge 8 commits into
FlowFuse:mainfrom
hmjvalineY:feat/i18n
Open

Add internationalisation (i18n) support to the platform UI#8312
hmjvalineY wants to merge 8 commits into
FlowFuse:mainfrom
hmjvalineY:feat/i18n

Conversation

@hmjvalineY

@hmjvalineY hmjvalineY commented Aug 27, 2026

Copy link
Copy Markdown

Adds internationalisation to the platform UI: the plumbing, full en and zh-TW locales, and a per-user language preference.

Written fresh against main rather than rebased from feature/i18n-implementation, which is now ~4,700 commits behind and pinned fastify-i18n@^1 (Fastify 4 era) and vue-i18n@^9. The architecture in that branch's docs/contribute/i18n.md is what this follows — library choices, locale directory layout, hierarchical key naming — with the code written against current APIs.

Related Issue(s)

#8359 (re-filed through the Feature Request template; #8311 was the same request opened as a blank issue and is now closed as a duplicate)

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 8 commits, and I am happy to split it if that is easier to review — see the note at the end.

Commit What
1 i18n infrastructure, zh-TW locale, User.language preference, login and sign-up pages
2 Sidebar, user menu, account settings page
3 The rest of the UI — templates, display attributes, JS-defined strings, validation errors, toasts, pluralised counts, role names
4 Audit log, welcome tour, instance state tiles, side-nav tabs
5 Text that wraps around interpolation
6 Regenerate frontend/src/types/generated.ts for the User.language field
7 Restore five pieces of English wording the extraction had changed
8 Give the frontend suite timeout headroom

Locale files: 2,879 keys, en and zh-TW in step.

What's here

Librariesvue-i18n@^11 on the frontend, fastify-i18n@^3 (over node-polyglot) on the server. Their interpolation syntaxes differ ({name} vs %{name}); that is documented rather than papered over.

Locale filesfrontend/src/locales/<locale>.json and locales/<locale>/common.json.

Both webpack entrypoints. webpack.config.js builds two separate Vue apps, main and setup. 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, then en. 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-i18n narrows a regional tag onto its base language (en-GB finds en) but does not widen a script-qualified tag onto a regional one, so zh-Hant-TW would fall back to English rather than finding zh-TW. Chrome reports exactly that tag for Traditional Chinese on some platforms. forge/i18n/locales.js maps 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 $t off, so frontend/src/i18n.js exports a bare t() for them.

Pluralisation. The ad-hoc pluralize() helper produced 1 Device / 2 Devices by appending s, which cannot work for a locale without plural forms. Those 15 call sites now use vue-i18n's own plural syntax, so en still reads correctly and zh-TW does too.

Audit log. data/audit-events.json 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. All 165 event ids resolve. The description sentences use named interpolation.

Locale parity tests. test/unit/forge/i18n/locales_spec.js asserts every locale defines the same keys as en, and that frontend/src/i18n.js and forge/i18n/locales.js agree on the supported set. A missing translation fails CI rather than silently rendering English. This mirrors what scripts/lint-colors.js already 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.vue branches on err.response.data.error === 'user registration not enabled', and test/unit/forge/routes/auth/index_spec.js matches the same literal. Translating them would break both. Those responses already carry a stable code field (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 is forge/postoffice/templates/.

RoleNames is 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 returning roleName and gains a separate translated roleLabel for the places a role is shown. A unit test caught this when I first changed roleName itself.

utils/pipelineValidation.js is untouched. Its header says it is maintained as a 1:1 copy of forge/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 as popup-type, the English label in SUPPORTED_LOCALES (deliberately in its own language), and content that comes from the database — team type names, instance types, stack labels.

User.language is stored server-side, unlike the theme preference which is local-only, because content generated outside a browser session needs it — most obviously the emails in forge/postoffice/templates. Happy to be redirected if you would rather this lived elsewhere; User has no generic preferences mechanism today, so this adds a flat column.

Known gap

21 strings inside runtime-compiled templates are still English. Some columns and cells are built as markRaw({ template: '...' }), and text inside those strings is invisible to the extraction. 18 of the 21 are in frontend/src/components/version-history/timeline/TimelineEvent.vue; the others are a "Read Only" badge in account/Security/Tokens.vue and unnamed / Don't assign in team/Settings/Devices.vue.

Most of the TimelineEvent ones are sentence fragments split around markup ("Flows deployed through the" + a link + "pipeline, applying the" + a link + "snapshot"), which is exactly the case <i18n-t> exists for — and <i18n-t> is not available inside a runtime template string. Doing this properly means restructuring those cells into real components, which is a different change with its own risk, so I have left it out rather than bolt it on to a PR this size. Flagging it so the coverage claim above is not read as absolute.

Build and test results

Environment: Windows 11, Node v24.14.0, npm 11.9.0, SQLite.

Rebased onto main at 2097e7b7a (the 3.0.1 release), so this branch carries the webpack 5.110.2, ldapts 9.0.0, nodemailer 9.0.6 and @sentry/vue 10.72.0 bumps that landed with it. npm run generate:types reports no additions against the committed generated.ts, so check-ts-drift has nothing to flag.

Every result below is from dd68870b4, the current tip, on FlowFuse 3.0.1 — the suites were re-run after the rebase rather than carried over.

Check Result
npm run lint 0 errors, 11 warnings — identical to the pre-change baseline on this commit (all 11 are pre-existing unused eslint-disable directives in test files)
npm run lint:colors pass
npm run test:unit:forge 3,346 passing, 24 pending, 0 failing (11m)
npm run test:unit:frontend 886 passing across 63 files, 0 failing
npm run test:system 61 passing, 0 failing
npm run build exit 0; both main and setup entrypoints emitted; same 2 pre-existing asset-size warnings
npm run generate:types no drift in frontend/src/types/generated.ts — see commit 6
npm run test:docs docs/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 test needed a timeout, and it is worth being precise about why. After the rebase, test/unit/frontend/services/app.orchestrator.spec.js began failing reproducibly (twice out of two) on Test timed out in 5000ms. It is not a hang and not a functional break:

Check Result
That spec alone, this branch 4 passed, 1.92s
That spec alone, main 4 passed, 1.86s
Same spec, --testTimeout=30000, full suite passes unchanged
Full suite on main 62 files, 878 passed

So the test sits close enough to the 5s default that suite load tips it over, and the locale catalogues add a few percent of import time — enough to turn an occasional failure into a repeatable one on my machine. Commit 8 sets testTimeout: 15000 in config/vitest.config.ts, in the config rather than on the one test so the reason stays visible.

I should correct an earlier framing of mine here: I first measured a loaded run of this branch against an idle run of main and read that as the suite getting ~50% heavier. Measured the same way, it is about 5%.

If you would rather not move that setting, the alternative is loading catalogues lazily in frontend/src/i18n.js so they stay out of the eager module graph. That is a larger change and it costs the synchronous bare t() this PR uses for strings outside components, so I have not done it unilaterally.

English output was checked for drift, not assumed. An extraction like this is only safe if an English reader sees no change, and reusing an existing key whose wording differs slightly breaks that quietly. So every $t('key') this branch introduces was compared against the text it replaced in that same file on main — 3,441 (file, key) pairs. After whitespace normalisation, 24 did not match; 17 of those are the pluralised messages and the new language-preference UI, 2 are the same sentence assembled differently, and 5 were real regressions, fixed in commit 7:

Where Was Had become
Account settings, email field label Email E-Mail Address
Personal access tokens, team scope line following team: / teams: fixed plural
Device group changes, snapshot notice Remote Instance / Instances fixed plural
Team pipelines intro, 2 of 3 copies full sentence truncated, losing ", and then, when you're ready, deploy your changes with a single click"
Role label for Roles.Dashboard Dashboard Dashboard Only

The last one is worth a note for anyone reviewing the pipelines page: application/Pipelines.vue really does use the shorter wording on main, so both keys are correct and only two call sites pointed at the wrong one.

Bundle cost: main 4.90 → 5.29 MiB, setup 4.86 → 5.19 MiB — the locale files ship in both entry bundles.

Two CI jobs I could not reproduce locally, so they remain unverified from my side: postgres-tests, which needs PostgreSQL 14, and ui-os-tests / ui-ee-tests, which need a mailpit container and — for the EE suite — a licence. The forge suite here ran on SQLite. Given that this touches 422 UI files, the Cypress suites are the ones I would most want to see, which is the other reason the CI approval matters.

Migration, on SQLite:

  • applies on startup, recorded in MetaVersions as the latest entry
  • PRAGMA table_info('Users') confirms language VARCHAR(255), nullable, default NULL
  • satisfies check-migrations.yml20260827-01 sorts last in the directory

Manual verification, browser locale zh-TW, against a database carrying real data (5 users, a team, a running Node-RED instance):

  • login, sign-up and the first-run setup flow render in Traditional Chinese with no configuration, from navigator.language alone; <html lang> is set correctly
  • forcing en renders English — both directions work
  • sidebar, user menu, account settings, team pages, admin pages, audit log and dialogs all render translated; browser tab titles too
  • <i18n-t> renders sentences with a link inside them in the right word order
  • validation errors and toasts set from component code render translated
  • PUT /api/v1/user with zh-TW → 200 and persists; fr-FR and nonsense400 from the schema enum; null → 200 and clears the preference
  • with localStorage deliberately set to en, a reload applies the stored zh-TW preference instead — precedence is correct

Checklist

  • I have read the contribution guidelines
  • Suitable unit/system level tests have been added and they pass
  • Documentation has been updated — docs/contribute/i18n.md
    • Upgrade instructions — n/a, the migration is additive and nullable
    • Configuration details — n/a, nothing in flowforge.yml changes
    • Concepts — locale resolution, key naming, and what is out of scope
  • Changes flowforge.yml? — no
    • Issue/PR raised on FlowFuse/helm to update ConfigMap Template — n/a
    • Issue/PR raised on FlowFuse/CloudProject to update values for Staging/Production — n/a

Labels

  • Includes a DB migration -> needs the area:migration label (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 the zh-TW values and land only the plumbing plus the en extraction, 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-TW locale current rather than land it and disappear.

@hmjvalineY

Copy link
Copy Markdown
Author

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:

  • pluralize() appends s to make a plural, which cannot work for a locale with no plural forms. The 15 call sites now use vue-i18n's own plural syntax.
  • RoleNames looked like display text but is compared against in code and feeds table search. The API keeps roleName and gains a separate translated roleLabel. A unit test caught this when I first changed roleName itself.
  • utils/pipelineValidation.js is a declared 1:1 copy of forge/lib/pipelineValidation.js, so it is left untouched — the backend copy cannot import the frontend locale.
  • fastify-i18n does not widen zh-Hant-TW onto zh-TW, which Chrome reports on some platforms. LOCALE_ALIASES handles it.

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 $t() substitution plus two locale files. Commit 1 stands alone as the infrastructure and is the part worth real scrutiny. If it is easier, I can split this into separate PRs, or drop the zh-TW values and land only the plumbing plus the en extraction. Happy to do whichever suits your review process — just say.

@hmjvalineY

Copy link
Copy Markdown
Author

Two housekeeping notes, neither needing a review.

CI has not run on this PR. All five workflows are sitting at action_required — Tests, Check DB migrations, SAST Scan, Publish Documentation, and Create pre-staging environment — so the PR reports no checks at all rather than passing or failing ones, and Tests summary is a required check. That is the first-time-contributor gate, and it needs a maintainer to approve the run. I would rather you saw CI's verdict than my own results, so whenever someone has a moment.

One thing worth flagging before that click: the approval releases all five, including Create pre-staging environment, which deploys. If you would rather not spin that up for a branch from a fork, say so and I will work out another way to get Tests reporting.

I re-filed the issue as #8359. The original, #8311, was opened as a blank issue, so it never picked up the needs-triage label the Feature Request template applies — it had been sitting outside triage rather than waiting in it. #8359 is the same request submitted through the template, and #8311 is now closed as a duplicate. #8359 also carries a correction: I had claimed fastify-i18n normalises script-qualified locale tags, and it does not, which is why forge/i18n/locales.js has a LOCALE_ALIASES map.

Nothing in the branch changed for either of these. It is rebased onto current main, and the results in the description are from after that rebase.

@hmjvalineY

Copy link
Copy Markdown
Author

Two commits since the last push, both from reproducing the CI jobs locally rather than waiting for the run to be approved. Neither changes what the feature does.

check-ts-drift would have failed. frontend/src/types/generated.ts is generated from the OpenAPI dump, and adding language to the PUT /api/v1/user body schema and the User view changes it. I had not regenerated it. Commit 6 does.

Worth knowing if you ever regenerate it outside CI: without DEV_ENTERPRISE_TIER_LICENSE the EE routes are not registered, so a local npm run generate:types drops ~3,290 lines of EE paths. The three lines in commit 6 are what the generator emits for this change, verified by diffing a local run against the committed file and confirming it leaves no additions, then applied on top so the EE paths stay intact.

Five pieces of English wording had changed. This is the one that bothered me more. An extraction like this is only safe if an English reader sees no difference, and pointing a string at an existing key whose wording differs slightly breaks that silently. So I compared every $t('key') this branch introduces against the text it replaced in the same file on main — 3,441 pairs. Five were real regressions:

  • the account settings email label read Email, and had been pointed at the sign-up form's E-Mail Address
  • the token team-scope line and the device-group snapshot notice had both lost their singular form, so one team read "teams:" and one instance read "Instances"
  • two of the three copies of the team pipelines intro had been pointed at the shorter variant, dropping ", and then, when you're ready, deploy your changes with a single click"
  • roleLabel(Roles.Dashboard) rendered Dashboard Only where capitalize(RoleNames[...]) rendered Dashboard

Commit 7 restores all five. On the pipelines one: application/Pipelines.vue genuinely uses the shorter wording on main, so both keys are correct and only the two call sites were wrong.

The same sweep found 21 strings still in English inside markRaw({ template: '...' }) runtime templates, 18 of them in TimelineEvent.vue. Most are sentence fragments split around markup, which is what <i18n-t> is for and which a runtime template string cannot use. That needs those cells restructured into real components, so I have left it out and written it up as a known gap in the description rather than bolt it on here.

Full suites re-run at 340f86838: lint 0 errors / 11 baseline warnings, forge 3,346 passing, frontend 886 passing, system 61 passing, locale parity 14 passing, build clean.

Still unverified from my side: postgres-tests needs PostgreSQL 14, and the Cypress suites need a mailpit container. For a change touching 422 UI files those are the ones I would most want to see, which is the other reason the workflow approval would help.

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>
`frontend/src/types/generated.ts` is generated from the OpenAPI dump, so
adding `language` to the `PUT /api/v1/user` body schema and to the User
view changes it. The Check TypeScript drift job would have failed on this
branch without it.

`npm run generate:types` boots the platform to dump the spec. Without
`DEV_ENTERPRISE_TIER_LICENSE` the EE routes are not registered, so a local
run drops ~3,290 lines of EE paths that CI keeps. The three lines added
here are exactly what the generator emits for this change - verified by
diffing a local run against this file, which leaves no additions - applied
on top of the committed file so the EE paths stay intact.

Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
Five places where swapping a literal for a message key altered what an
English reader sees. Found by comparing every `en` value this branch
introduces against the text it replaced in that same file on `main`.

- Account settings: the email field label read "Email". It had been
  pointed at `common.fields.email`, which is the sign-up form's
  "E-Mail Address". Now `ui.email`.
- Personal access tokens: the team scope line was
  ``following ${pluralize('team', n)}:`` and had become a fixed plural.
  Now a pluralised message taking the count.
- Device group changes: the snapshot notice was
  ``Remote ${pluralize('Instance', n)}`` and had become a fixed plural.
  Now a pluralised message taking the count.
- Team pipelines: two of the three copies of the intro paragraph had been
  pointed at the shorter variant, dropping ", and then, when you're
  ready, deploy your changes with a single click".
  `application/Pipelines.vue` genuinely uses the short wording on `main`,
  so both keys are correct - only these two call sites were wrong.
- Role labels: `roleLabel(Roles.Dashboard)` rendered "Dashboard Only"
  where `capitalize(RoleNames[Roles.Dashboard])` rendered "Dashboard".
  Now `ui.dashboard`. All six roles now render the previous English.

Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
`app.orchestrator.spec.js` times out at the 5s default when the whole
suite runs, while passing in 1.9s on its own - on this branch and on
`main` alike, measured the same way. The locale catalogues add a few
percent to the suite's import time, and that was enough to make the
timeout reproducible here rather than occasional.

The test is not hanging: raising the timeout to 30s let it pass
unchanged, and the failure is load-dependent, not deterministic. Setting
`testTimeout` in the config rather than on the one test keeps the reason
visible instead of burying it in a spec file.

If you would rather not move this, the alternative is to make
`frontend/src/i18n.js` load catalogues lazily so they stay out of the
eager module graph. That is a larger change and it would cost the
synchronous bare `t()` this PR relies on for strings outside components,
so I have not done it here - happy to if you prefer.

Signed-off-by: hmjvalineY <hmjvaline@ymhs.tyc.edu.tw>
@n-lark n-lark added the area:migration Involves a database migration label Sep 2, 2026
@n-lark

n-lark commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Hey @hmjvalineY thank you for the contribution, we appreciate you taking this on.

We'd like to take you up on your offer to split this, and in smaller pieces than the existing commits. A change this size isn't something we can review meaningfully in one pass, and iterative improvement is one of our core values. Smaller PRs mean we can land each piece as it's ready instead of sitting on all of it.

On CI, noted. Once this is split into smaller parts we'll look at getting all of that set up. I've added the area:migration label. Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:migration Involves a database migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants