Translating
Read a message with useTranslation, or place one declaratively with <T>. Both are reactive — switch the locale and everything re-renders.
useTranslation
import { component } from 'sigx';
import { useTranslation } from '@sigx/i18n';
const Cart = component(() => {
const t = useTranslation('cart');
return () => (
<div>
<h1>{t('title')}</h1>
<p>{t('items', { count: items.value.length })}</p>
</div>
);
});
The namespace is checked against the generated schema, and so are the keys. Call it without one and you get defaultNamespace ('translation' unless you changed it).
Pin a call to a locale that is not the active one — a preview pane, a row that carries its own locale — with the locale option:
const tSv = useTranslation('cart', { locale: 'sv' });
<T>
The declarative form, for placing a message in markup:
<T k="cart.items" params={{ count }} />
<T ns="content" k={block.labelKey} default={block.label} />
<T k="cart.title" locale={row.locale} />
It is renderer-agnostic — on lynx it goes inside a <text> element exactly as any other string would:
<text><T k="cart.title" /></text>
Rich text
A message that needs markup in the middle of a sentence cannot be split without breaking translation — word order differs per language. Pass components and the tags in the message are mapped to nodes:
<T k="legal.terms" components={{ a: (c) => <a href="/terms">{c}</a> }} />
{ "legal": { "terms": "Please read our <a>terms of service</a> before continuing." } }
The splitting is non-nested, and an unknown tag stays literal. You supply the nodes, which is what keeps this renderer-agnostic — the library only positions them, so a lynx app passes lynx elements and the same message works.
renderRich(text, components) is the underlying function if you need it directly.
Namespaces
A namespace is a unit of loading. List the ones your first paint needs:
createI18n({
fallbackLocale: 'en',
namespaces: ['common', 'nav'], // loaded up front
load: (target, locale, ns) => import(`./locales/${target}/${locale}/${ns}.json`),
});
Leave section-specific namespaces out and they load on first use, so a settings page's catalog does not sit in the initial payload.
Fallback
fallbackLocale is the master locale — the source of truth for which keys exist. A key missing from the active locale falls through to it; onMissing decides what happens when it is missing from both.
Resolution walks a chain: BCP-47 truncation (sv-FI → sv → the fallback), plus any explicit localeFallbacks you declare.
A message is always formatted in the locale it was found in. That matters for plurals: a Swedish string found in the Swedish catalog selects its plural form by Swedish rules, even if the request asked for sv-FI.
Layers
layers turns on per-key layering, for the case where a tenant or a brand overrides a handful of messages and everything else should keep falling through:
createI18n({
fallbackLocale: 'en',
layers: ['base', 'tenant'], // lowest priority first
loaders: { base: loadBase, tenant: loadTenant },
});
An override layer supplies a few keys; every other key falls through to the layer below, including keys the base gains in a later version — so a tenant override does not freeze that tenant's catalog at the version it forked from.
Layers compose within one locale and the locale chain is walked after. A base message in the requested locale therefore beats a tenant override that exists only in a fallback locale — which keeps the message formatted in the locale it was found in.
Omit layers entirely for single-source behaviour: nothing is composed and nothing is allocated.
Next steps
- Locales — detection, switching and persistence.
- Runtime catalogs — keys that are not known at build time.
