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, plus failure, which turns what went wrong into text. 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 single plain object argument (an object literal, or one with a null prototype) is the named arguments; any other single argument, aDateor anErroramong them, fills{0}. A placeholder name is letters, digits and underscores:{amount}interpolates,{user-name}stays literal. - 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 page 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 -->
<p>{{ #l10n:failure(error) }}</p> <!-- the reasons of a Failure, one per line -->The module reads l10n and locale from the data the template is bound to, the registry's overlays here (the most recent one carrying the name wins), or the data handed to a Template.* factory. It never reads them from what a render pushes on top: a row of a data-tpl-each, a response, a withOverlay(...). A row that happens to carry a field named l10n or locale still shows as data ({{ locale }}), and changes neither the messages nor the formatting. Translations are page source; rows are what arrives later.
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). failure reads a value carrying a non-empty problems array (an httpc Failure) as their reasons, one per line, a blank or missing reason reading as the failure.no-reason message; any other value with a message reads as that message, anything else as its string form, and null or undefined as the empty string. It is what the dialog, the drawer, the table and the async sections paint for a rejection, and what a toast shows for a failure.
Localization.of(overrides?) resolves the active { l10n, locale } from the registry data (most recent overlay wins) and returns bound { t, date, number, bytes, failure }:
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 together with locale, the language in its canonical form: IT-it and the underscore form a server prints, it_IT, both publish it-IT. An empty language, which is what document.documentElement.lang answers on a page without the attribute, counts as none given and falls back to the browser's language, then to en. A language that is no language tag at all throws a RangeError from the Plugin constructor, where the page configures, rather than from the first number formatted in it. language is matched on its primary subtag, so it-CH reads the it built-ins, and a language with no built-ins of its own reads in english. Built-ins ship for en, it, es, fr and cover every ful message: the keys are listed below.
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.
The keys of the l10n overlay the ful plugin bakes, each overridable through translations:
| Key | Used by |
|---|---|
pagination.showing, pagination.previous, pagination.next, pagination.reload, pagination.navigation
|
ful-pagination |
table.initial, table.error, table.no-data
|
ful-table states |
dropdown.empty, select.remove
|
ful-select |
files.dropzone-label, files.remove, files.unacceptable-file-type, files.max-file-size-exceeded, files.max-total-size-exceeded, files.max-files-exceeded
|
ful-input-file |
filters.op.* (one per operator), filters.sensitivity.*, filters.boolean.*
|
ful-filter-* |
info.tooltip |
ful-tooltip |
dialog.acknowledge, dialog.close, dialog.confirm, dialog.cancel
|
ful-dialog (dialog.confirm and dialog.cancel label the buttons of Dialog.confirm) |
drawer.close |
ful-drawer |
dialog.discard.header, dialog.discard.body, dialog.discard.confirm, dialog.discard.cancel
|
the question guard-dismiss asks on a ful-dialog or a ful-drawer before a dismissal drops unsaved edits |
spinner.loading |
the label of a ful-spinner in a submitting ful-form, a loading ful-table, ful-select, ful-dialog and ful-drawer
|
tabs.previous, tabs.next
|
the scroll buttons of a ful-tabs whose tabs do not fit |
toast.region, toast.dismiss
|
ful-toasts |
wizard.progress |
ful-wizard |
failure.no-reason |
Localization.failure, for a problem with no reason |
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, and ftl:ready waits with them: it fires once the elements a late configure() defines have upgraded. 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