Skip to main content

Internationalization

Every label the framework renders — menu entries, screen titles, dictionary column labels, dialog buttons, error messages — goes through a single resolution path: the active language, picked per request from one of three sources. The same path covers the React frontend (UI chrome translated via react-i18next) and the backend (dictionary labels resolved server-side and returned with the API payload).

This page covers the resolution order, the format of a language pack, how to add a new language and how to override labels per app or per environment.


At a glance

Language resolution order — first match wins1 · REQUEST HEADERX-Liberty-Lang: frset by the frontend2 · USER PREFERENCEusers.alice.lang = "fr"stored on the user record3 · DEFAULT[app] default_lang = "en"install-wide fallback

How a label is resolved

A label in dictionary.toml carries one value per language:

[columns.invoice_number]
label = { en = "Invoice number", fr = "Numéro de facture", de = "Rechnungsnummer" }

When the framework returns a screen definition, it picks the active language, looks the label up in the map, and serialises only the resolved string. A missing language falls back to:

  1. The active language exactly (fr).
  2. The active language stripped of its region (fr from fr-CA).
  3. The default language (en if [app] default_lang = "en").
  4. The first available language in the map.

A label with no map at all (label = "Invoice number") is returned verbatim — used for technical labels that don't need translation.

The same resolution is used for description, placeholder, tooltip, and every other user-facing string in the dictionary.


The active language

SourceWhen it applies
X-Liberty-Lang request headerSent by the React frontend on every API call. Reflects the language picker in the header. Overrides everything else.
User preference (users.<name>.lang)Stored on the user record. Applied when the request has no header — typically a direct API call (CLI, integration).
[app] default_langThe install-wide default. Applied when no header and no user preference. Defaults to "en" when not set.

The X-Liberty-Lang header overrides the user preference deliberately — a user signed in with fr who switches the header dropdown to en sees the UI in English without changing their stored preference.


Supported languages

The framework ships with English (en) and French (fr) UI translations. Every other language can be added by dropping a language pack — see below. The Settings UI lists every loaded language in the header picker; languages are loaded at framework startup.

The two-letter ISO 639-1 codes are the convention (en, fr, de, es, it, …). Regional variants are accepted (fr-CA, en-GB); the resolution falls back to the base code when the regional variant isn't defined.


Adding a new language

A language pack is a JSON file per surface. The framework expects them under liberty-apps/i18n/<lang>/:

liberty-apps/
└── i18n/
└── de/
├── common.json ← framework chrome (buttons, errors, dialogs)
├── apps.json ← workspace selector labels
├── billing.json ← per-app overrides
└── crm.json

common.json — framework chrome

{
"actions": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"refresh": "Aktualisieren"
},
"errors": {
"required": "Pflichtfeld",
"invalid_format": "Ungültiges Format"
}
}

The keys are stable across framework versions; new keys land in the bundled en and fr packs and need translating when added. The framework's CI exports the diff between en and every other language on each release as a translation backlog.

Per-app overrides

billing.json overrides any label the billing app declared in dictionary.toml. Useful when:

  • You want a different wording for one app without touching the central dictionary.
  • A packaged vendor app ships only en and fr; you add a de translation install-side.
{
"columns.invoice_number.label": "Rechnungsnummer",
"columns.due_date.label": "Fälligkeitsdatum",
"menus.billing.label": "Rechnungsstellung",
"screens.billing.invoices.title": "Rechnungen"
}

The keys are the dotted path into the central TOML — the same path the Rename operation uses.

Activation

After dropping the files, restart the framework (language packs are loaded once at startup). The Settings UI's Languages page lists every detected pack with its key count; a partial pack is fine — missing keys fall back to the resolution chain.


Override priority

A label can be defined in several places. The resolution order (first wins):

  1. Per-app pack in liberty-apps/i18n/<lang>/<app>.json.
  2. Inline label map in the dictionary entry of dictionary.toml.
  3. Common pack in liberty-apps/i18n/<lang>/common.json (for framework chrome).
  4. Bundled pack in liberty-next/liberty/i18n/<lang>/*.json (the framework's own defaults).

A customer that wants to rename the Save button to Submit for one app does it in liberty-apps/i18n/en/<app>.json without editing the framework or the central dictionary:

{ "actions.save": "Submit" }

API access to translations

The frontend bundles translations for the active language at build time; the backend serves the same translations via GET /api/i18n/<lang> for runtime consumers (Excel exports, PDF generation, etc.):

GET /api/i18n/fr
X-Liberty-Lang: fr

→ 200 OK
{
"actions": { "save": "Enregistrer", "cancel": "Annuler", ... },
"errors": { "required": "Champ obligatoire", ... },
"apps": { "billing": "Facturation", ... },
"billing": { "columns.invoice_number.label": "Numéro de facture", ... }
}

The response is cached server-side and tagged with the i18n bundle's revision — clients re-fetch only when the revision changes (after POST /admin/reload for the i18n scope).


Right-to-left languages

The framework's React layer respects the dir="rtl" attribute when the active language is one of ar, he, fa, ur. Mirror-friendly icons, padding and table alignment switch automatically. SVG mockups embedded in screens don't mirror — you'll need a per-direction variant if a flow diagram must read right-to-left.


Tips & best practices

  • Translate dictionary entries inline, framework chrome via packs. The dictionary is where every domain label lives; common UI strings belong in shared packs.
  • Keep the keys stable. Renaming a dictionary entry breaks every per-app translation override; use the Settings UI's Rename operation, which rewrites the language packs in lockstep.
  • Run the translation backlog after each release. The bundled en pack is the reference; liberty-admin i18n-diff fr lists every key missing from the French pack.
  • Don't translate technical labels. Database column names, error codes, log messages keep their English-only form — they're for operators, not end users.
  • Test under each language. A long German label breaks tight table columns; flipping the UI to de catches it.

What's next

  • Apps — workspace selector that uses the apps translation namespace.
  • Concepts → Dictionary — where inline label = { … } maps are declared.
  • REST API referenceGET /api/i18n/<lang> and the X-Liberty-Lang header contract.