Repository navigation
06 Localization
Localization is a plain template module: one flat translations map per page, named placeholders, { one, other } plural leaves, and locale-aware date/number/bytes formatters. It works in bare ftl pages and, through the ful plugin, in ful pages.
{
'pagination.showing': 'Page {current} of {total}',
'pagination.next': 'Next',
'app.added': { one: '{count} item added', other: '{count} items added' },
}- flat, dotted keys, with no nesting.
-
{name}placeholders interpolate the named arguments;{0},{1}… interpolate positional ones. - a plural leaf is an object keyed by CLDR plural category (
zero,one,two,few,many,other);otheris required, the rest fall back to it. The category is selected withIntl.PluralRulesover a numeric{count}argument.
Failures do not throw: a missing key returns the key itself, a missing placeholder stays literal, a plural without {count} falls back to other. Each is warned once per session on the console.
ful registers the module for you; a bare ftl page registers it by hand:
ftl.registry
.defineModule('l10n', ftl.Localization)
.defineOverlay({ l10n: { 'app.hello': 'Hello {who}' }, locale: 'it' })
.configure();<p>{{ #l10n:t('app.hello', { 'who': 'ada' }) }}</p>
<p>{{ #l10n:t('app.added', { 'count': 3 }) }}</p>
<p>{{ #l10n:date(now, { 'dateStyle': 'long' }) }}</p>
<p>{{ #l10n:number(42.5, { 'style': 'currency', 'currency': 'EUR' }) }}</p>
<p>{{ #l10n:bytes(1536) }}</p> <!-- 1.5KiB, digits in the page locale -->date, number take the same options as the matching Intl formatter, honoring the page locale; bytes formats with maximumFractionDigits: 2 and B/KiB/MiB/GiB suffixes, a size exactly on a threshold taking the larger unit (1024 is 1KiB).
Localization.of(overrides?) resolves the active { l10n, locale } from the registry data (most recent overlay wins) and returns bound { t, date, number, bytes }:
const { t } = ftl.Localization.of();
console.log(t('app.added', { count: 3 }));An explicit Localization.of({ locale: 'de' }) overrides the locale for the formatters.
The language is fixed per page and baked once at configure, together with the fallback chain:
ftl.registry
.plugin(new ful.Plugin({
language: 'it', // full BCP-47 tag or primary subtag; defaults to the browser's language
translations: { // flat, active-language only
'pagination.next': 'Avanti ancora', // reword a built-in
'checkout.total': 'Totale: {amount}', // or add your own keys
},
}))
.configure();The overlay is built as { ...builtins.en, ...builtins[language], ...translations }, registered as the l10n overlay plus the full-tag locale. Built-ins ship for en, it, es, fr and cover every ful message (pagination, table states, select, files and filters), the full list being in ful fields.
External components ship their translations the same way: the page slices the active language out of the component's dictionary and hands it over next to its own keys.
Remote translations are the app's concern. Fetch them before configure():
const translations = await (await fetch(`/l10n/${language}/overrides.json`)).json();
ftl.registry.plugin(new ful.Plugin({ language, translations })).configure();Elements are not even defined until configure() runs, so nothing upgrades, or renders, ahead of its translations. Latency-sensitive pages skip the network and server-render the map inline instead: translations: window.APP_L10N.
Bare ftl pages that fetch remotely follow the same rule: await the fetch, then defineOverlay({ l10n, locale }), then configure().
- ful getting started for the plugin in context.
-
examples/ftl/l10n.htmlin the repo.
ftl
httpc
ful
- Getting started
- Fields
- Forms
- Inputs
- Select
- Files
- Temporal inputs
- Tables
- Disclosures
- Navigation
- Async sections
- Timing and storage
- Theming
Extending
Reference
Project