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
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:
- The active language exactly (
fr). - The active language stripped of its region (
frfromfr-CA). - The default language (
enif[app] default_lang = "en"). - 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
| Source | When it applies |
|---|---|
X-Liberty-Lang request header | Sent 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_lang | The 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
enandfr; you add adetranslation 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):
- Per-app pack in
liberty-apps/i18n/<lang>/<app>.json. - Inline
labelmap in the dictionary entry ofdictionary.toml. - Common pack in
liberty-apps/i18n/<lang>/common.json(for framework chrome). - 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
enpack is the reference;liberty-admin i18n-diff frlists 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
decatches it.
What's next
- Apps — workspace selector that uses the
appstranslation namespace. - Concepts → Dictionary — where inline
label = { … }maps are declared. - REST API reference —
GET /api/i18n/<lang>and theX-Liberty-Langheader contract.