Skip to main content

Internationalisation

blong-browser has a lightweight translation system built on appStore, and the convention that makes it light is that the key is the English string. A missing translation therefore degrades to English rather than to a key nobody can read, and a missing language degrades to the same place.

Translate a label​

<Text> is the primitive. Its string child is both the key and the fallback:

import {Text} from '@feasibleone/blong-browser';

<Text>Save</Text>
<Text params={{field: 'Name', minLength: 3}}>
{'{field} must be at least {minLength} characters'}
</Text>

useText (and useTranslate for a callback) resolve the key against the active language:

// the whole fallback rule
let text = translations[id] ?? id;

params are interpolated after that lookup, so {field} is filled in whatever the sentence became. Because the English sentence is the key, an abstract key like buttons.save would break the promise — it would render as buttons.save when it is missing.

The blong Button wrapper translates its string label the same way, so importing Button from blong-browser rather than from PrimeReact is what gives you a translated button:

import {Button} from '@feasibleone/blong-browser';
<Button
label="Save"
icon="pi pi-check"
/>;

Not every control in the library goes through it: the editor's built-in toolbar buttons are raw <button aria-label="…"> elements with English names, so a fully translated UI has to account for them (or replace them with action buttons, which do translate).

Add a language​

A realm declares the languages it offers and the translations it has, in its browser entry:

// realm/browser.ts
config: {
default: {
portal: {
languages: [
{value: 'en', label: 'English'},
{value: 'bg', label: 'Български'},
],
translations: {
en: {},
bg: {Save: 'Запази', '{field} is required': '{field} е задължително'},
},
},
},
},

Three shapes are worth knowing. translations is keyed by language and applied per language, so switching to a language with an empty dictionary leaves the English strings in place. The same translations can be registered imperatively with useAppStore.getState().setTranslationsByLanguage({bg: {...}}). And a realm contributes to a merged configuration rather than owning it: mergePortalConfigs concatenates menus, merges same-titled groups, and takes the first value it finds for the scalars — so several realms can each add their own dictionary.

PrimeReact's own widget strings (date pickers, data tables) are a separate table, registered through IThemeConfig.languages, which Theme passes to addLocale:

<App
handlerProxy={handlerProxy}
theme={{
name: 'lara-light-blue',
languages: {
bg: {
accept: 'Да',
cancel: 'Отказ',
emptyMessage: 'Не са открити резултати',
dateFormat: 'dd/mm/yy',
firstDayOfWeek: 1,
// … the rest of the locale, from primefaces/primelocale
},
},
}}
/>

setLanguage('bg') then activates both halves: the application strings through the store, and the widget strings through locale('bg') in Theme.

The switcher​

LanguageSwitcher is rendered in the portal's menubar and takes its options from portalConfig.languages, falling back to the keys of portalConfig.translations. It renders nothing when fewer than two languages resolve, so a portal with one language has no control to explain. Selecting an entry calls setLanguage immediately; the choice is client-side and is not persisted — persisting a preferred language is the profile page's job.

Validation messages​

Schema validation messages are templates in the same convention, translated with the label of the field they are about: validate.ts holds the message table, and the {field} placeholder is resolved to the field's translated label by the form. That is why a translated field name appears inside a translated sentence.

Preview a story in another language​

A Storybook story takes a lang arg, and the dispatcher decorator applies it — dictionary, locale and language — for that story alone:

export const ToolbarBG: Story = {...Toolbar};
ToolbarBG.args = {lang: 'bg'};

The toolbar's Language item switches every story at once, through useAppStore.setLanguage. The two do not fight: only a value the reviewer changed in the toolbar is applied, so a story that names its own language keeps it until someone reaches for the item. The item's own list defaults to en and bg and can be widened with the toolbar options.

That is how a component's behaviour in another language is reviewed next to its English story, without a running suite in the background. The same story, opened with the language already set to bg — which is what reading the page with a ?globals=language:bg URL does to any story:

Editor/Toolbar opened in Bulgarian through the toolbar's Language globalOpen in Storybook

Testing translation​

A test that switches the language is asserting on strings, so it has to switch back: the storybook dispatcher and the unit tests reset the dictionary and the language in a finally, and the Playwright suite asserts the translated accessible names (the language-switcher spec in realm/blong-access/ drives the real portal in both languages).

See also​

  • blong-browser pattern — the realm, page and menu composition this sits in
  • Browser UI concept — the App → BlongProvider → Theme → Portal stack
  • blong-i18n skill — the same mechanism as a task card, for agents