Skip to content

Latest commit

 

History

History
176 lines (132 loc) · 6.43 KB

File metadata and controls

176 lines (132 loc) · 6.43 KB
navTitle Internationalisation
meta
description tags
How translatable strings are handled in the FlowFuse platform, and how to add or extend a locale.
flowfuse
i18n
internationalisation
localisation
contributing

Internationalisation

The platform UI is translatable. This page covers where the strings live, how a locale is chosen at runtime, and what to do when you add a string or a language.

Libraries

Side Library Interpolation syntax
Frontend vue-i18n {name}
Backend fastify-i18n (wrapping node-polyglot) %{name}

The two interpolation syntaxes are different. This is a property of the underlying libraries, not a choice — copying a message from one side to the other means rewriting its placeholders.

Where strings live

locales/<locale>/common.json          server-side messages
frontend/src/locales/<locale>.json    browser-side messages

Locale codes are BCP 47 tags, so zh-TW rather than zh_TW or tw.

The list of locales the platform ships is declared twice, because the frontend is bundled separately from the server:

  • forge/i18n/locales.jsSUPPORTED_LOCALES
  • frontend/src/i18n.jsSUPPORTED_LOCALES

test/unit/forge/i18n/locales_spec.js asserts the two agree, and that every locale defines exactly the same keys as en. A missing translation is a test failure rather than something that silently renders English in production.

Key naming

Keys are hierarchical namespaces grouping semantically related messages:

{
  "common": {
    "actions": { "login": "Login" },
    "errors": { "requiredField": "Required field" }
  },
  "auth": {
    "login": {
      "forgotPassword": "Forgot your password?",
      "errors": { "loginFailed": "Login failed" }
    }
  }
}

Put a message under common when more than one page uses it, and under a page-specific namespace otherwise. Keys are sorted alphabetically within each object — this keeps diffs readable when locales are edited in parallel.

Using a message

In a template:

<label>{{ $t('common.fields.password') }}</label>

In component code — both Options and Composition API, since the plugin is registered with globalInjection:

this.errors.general = this.$t('auth.login.errors.loginFailed')

When a message contains markup — a link inside a sentence — do not split it into fragments. Word order differs between languages, and fragments cannot be reordered. Use <i18n-t> with a named slot so the translator controls where the markup lands:

<i18n-t keypath="auth.signUp.tcs" tag="span" scope="global">
    <template #termsLink>
        <a :href="url">{{ $t('auth.signUp.tcsLink') }}</a>
    </template>
</i18n-t>
{ "tcs": "I accept the {termsLink}" }

scope="global" is required in components that do not call useI18n().

Messages read inside data() are a trap: prefer a computed, so the value is re-evaluated when the locale changes rather than frozen at construction.

How a locale is chosen

In order of precedence:

  1. The user's stored preference — User.language, set on the account settings page
  2. The request or browser locale — Accept-Language on the server, navigator.language in the browser
  3. en

Steps 1 and 2 exist separately because the login and sign-up pages render before there is a session, so there is no stored preference to read. The browser locale is what makes those pages translatable at all.

resolveLocale() in frontend/src/i18n.js narrows an arbitrary tag onto one we ship — zh-Hant-TW and zh both resolve to zh-TW — so an unrecognised tag falls back rather than rendering raw keys.

The server needs a little help to match that. fastify-i18n narrows a regional tag onto its base language, so en-GB finds en, but it does not widen a script-qualified tag onto a regional one: zh-Hant-TW would fall back to English rather than finding zh-TW. Chrome reports exactly that tag for Traditional Chinese on some platforms, so forge/i18n/locales.js declares LOCALE_ALIASES mapping those tags onto the canonical locale. Add an entry there if a locale you add has script-qualified variants in the wild.

A user with no stored preference keeps whatever the browser negotiated. Signing in must not quietly reset someone to English.

Adding a locale

Taking de as the example:

  1. locales/de/common.json — copy locales/en/common.json and translate
  2. frontend/src/locales/de.json — copy frontend/src/locales/en.json and translate
  3. Add 'de' to SUPPORTED_LOCALES in forge/i18n/locales.js
  4. Add { value: 'de', label: 'Deutsch' } to SUPPORTED_LOCALES in frontend/src/i18n.js, and import the file into messages
  5. Run npm run test:unit:forge — the parity test will name any key you missed

Write the label in the language itself. Someone looking for their own language should not have to read English to find it.

Adding a string

Add the key to every locale file, not just en. The parity test fails otherwise. If you cannot translate it, that is a signal the locale needs a maintainer for that language — not a reason to skip the key.

Deliberately out of scope

API error strings are not translated. Some of them are part of the de-facto contract: frontend/src/pages/account/Create.vue branches on err.response.data.error === 'user registration not enabled', and a unit test matches the same literal. Translating those strings would break both.

Those responses already carry a stable code field — user_registration_unavailable in that example — which is what callers should branch on. Migrating consumers to code, and only then translating the human-readable error text, is a separate piece of work.

The server-side plumbing is registered and request.i18n.t() is available, so that work does not need to start from nothing. The natural first consumer is forge/postoffice/templates/, where the output is read by a person and parsed by nobody.

Not yet handled

  • Pluralisation. Both libraries support it; no message needs it yet. Reach for the library's own plural syntax rather than branching in a component.
  • Date, time, and number formatting. Still locale-independent.
  • Right-to-left layouts. No RTL locale ships yet; the CSS has not been audited for it.