Frontend Multi-language via Curated Catalogs (vue-i18n), Not Runtime Machine Translation
Context
Both Vue 3 SPAs (frontends/user, frontends/admin) ship with zero i18n: page chrome is hardcoded English while the shared error-message catalog (src/services/messages/zh-CN.ts) is hardcoded Chinese with DEFAULT_LOCALE = 'zh-CN' — the rendered UI is mixed-language today. The docs site already runs a dual-locale convention (Docusaurus defaultLocale: 'en' + a zh-CN mirror tree, same-PR dual writes). We evaluated automatic-translation widgets — primarily translate.js (xnx3/translate) — against curated message catalogs before investing in an extraction pass over all 27 views.
Key findings on translate.js (v4, MIT, actively maintained): it walks the DOM after load and sends page text to the author's proprietary cloud (api.translate.zvo.cn et al.) for machine translation; the free channel has a daily character cap; translation happens after first paint (FOUC by design); the Vue adapters have open unresolved issues (#54, #94); and it rewrites input value attributes — a UX hazard on credential forms.
Decision
Adopt curated message catalogs with vue-i18n v11 (Composition API, legacy: false, globalInjection: true) as the single i18n mechanism for both frontends. Locale set: en (default) + zh-CN now, extensible to a tier-2 set (zh-TW, ja, de, es, fr, pt-BR, ru) later. Runtime machine-translation widgets are rejected for the product UI.
Rationale: fulla is a security-sensitive identity provider — consent/scope/authorization terminology must be curated, page content must not egress to third-party translation clouds, self-hosted/air-gapped deployments (the IdP norm) must work offline, and rendered text must be deterministic and reviewable in PRs. translate.js is MIT-licensed and could technically be embedded, but its data flow alone disqualifies it for this product. vue-i18n is the de-facto Vue standard (MIT, ~3.9M downloads/week), needs no extra build tooling at our catalog size (~10–14 KB brotli runtime), and matches the existing per-locale resource-file design of the error catalog.
Consequences and Current State
- One language switcher per app drives page chrome and newly surfaced error messages:
getErrorMessage(code)resolves in the active UI locale at the moment the error triggers. An already-rendered message is a snapshot — switching locale afterwards does not re-translate text on screen (this behavior is locked by e2e; a fully reactive rewrite that stores error codes and re-resolves at render is a tracked follow-up option).DEFAULT_LOCALEbecomes the fallback table (en). - Locale is detected from
localStorage['fulla-locale'](mirrors thefulla-themepattern) →navigator.languages→en, applied synchronously before mount (no flash), and synced to<html lang>. - Catalogs are dual-written (
en+zh-CN) in the same PR, enforced by key-parity unit tests; the mirrored-services andcomponents/uibyte-sync gates keep both apps identical where shared. - Machine translation remains permissible in the translation workflow (pre-translation with human review when adding a locale), never in the runtime.
- One-time cost: extraction of all user-visible strings across 27 views plus e2e assertions that pinned Chinese error text move to the English catalog (default locale); both languages are then covered by a dedicated
i18n.spec.tsper app.