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:
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 → Portalstack blong-i18nskill — the same mechanism as a task card, for agents