i18n
ICU MessageFormat + CLDR Plural Rules + locale-aware Intl formatters with translation lookup. Built on Node 24's bundled Intl.* (PluralRules, NumberFormat, DateTimeFormat, RelativeTimeFormat, ListFormat, DisplayNames) — zero vendoring, zero CLDR data shipped, the runtime owns it.
Lookup chain: t("nav.home", vars, { locale }) walks the subtag-stripped chain (pt-BR → pt), then falls through to the configured fallbackLocale and finally defaultLocale unless fallbackLocale: null (strict "this locale or miss"). Plural- shaped values use CLDR cardinal keys (zero / one / two / few / many / other); other is mandatory and validated at load. Ordinal plurals route through a separate Intl.PluralRules({ type: "ordinal" }) cache via to(key, count).
Translation file format (JSON loaded eagerly from opts.dir or inline via opts.translations):
{ "greeting": "Hello, {name}!", "items": { "one": "{count} item", "other": "{count} items" }, "nav": { "home": "Home", "about": "About" } }
ICU MessageFormat ({name, plural, ...} / {name, select, ...} / {name, selectordinal, ...}) is auto-detected by t(); operators force the path with t(key, vars, { messageFormat: true }). The companion b.i18n.messageFormat namespace exposes the parser / formatter for use outside an instance.
Validation policy: - create() throws on bad opts (boot). - Bad BCP 47 locale at any boundary → throw at call site. - t(missingKey) → return the key + emit i18n.missing observability event (never throws unless missingKey: "throw"). - Plural shape missing other → throw at load time. - Missing interpolation var renders as literal {var} unless interpolation.strict: true. - formatNumber / formatDate / formatRelative / formatList throw at call site on a non-finite value or unparseable date. - Middleware Accept-Language parse error falls back to the default locale; the request never crashes on a bad header.
Security stance: translation values come from operator-controlled files, not user input. {var} interpolation does NOT html-escape; b.template already escapes at render time. For non-template contexts, pass interpolation.escape: fn.
b.i18n.localeChain(locale, opts) #
{
defaultLocale: string, // BCP 47; required; the final baseline
fallbackLocale: string | null, // null = strict; omitted = defaultLocale
locales: string[], // optional configured set; when given, defaultLocale + fallbackLocale must be members
}
Resolve a requested BCP 47 locale to its ordered fallback chain — the same subtag-strip logic the file-backed t() lookup uses, surfaced as a pure, instance-independent function so a data-backed consumer (translations in a database row, a CMS page, a CDN-cached asset) can drive its own per-(resource, locale, field) lookups on the framework's BCP 47 fallback instead of re-deriving subtag stripping by hand.
Pure: no file loading, no message lookup. The chain is the requested locale, then each subtag-stripped parent ("fr-CA" -> "fr"), then fallbackLocale, then defaultLocale, de-duplicated. fallbackLocale: null gives strict "this locale or its parents only" (no cross-locale jump).
b.i18n.localeChain("fr-CA", { defaultLocale: "en", fallbackLocale: "en" });
// → ["fr-CA", "fr", "en"]
b.i18n.localeChain("zh-Hant-TW", { defaultLocale: "en", fallbackLocale: null });
// → ["zh-Hant-TW", "zh-Hant", "zh"] (strict — no jump to en)
b.i18n.create(opts) #
{
defaultLocale: string, // BCP 47 tag; required, must appear in locales
locales: [string], // BCP 47 tags; required, non-empty
fallbackLocale: string | null, // null = strict; default = defaultLocale
translations: { [locale: string]: object }, // inline trees (mutually exclusive with dir)
dir: string, // load /.json (mutually exclusive with translations)
eagerLocales: [string], // with lazyLoad: which locales to load at create
lazyLoad: boolean, // with dir: load other locales on first lookup; default false
interpolation: { start?: string, end?: string, escape?: fn, strict?: boolean },
missingKey: "return-key" | "throw" | (key, locale) => string,
onMissingKey: (key, locale) => void, // observability hook (best-effort)
rtlLanguages: [string], // override the framework default RTL list
observability: { event: (name, value, labels) => void },
clock: () => number, // ms-since-epoch override (testing)
}
Build an i18n instance bound to a fixed locales set. The returned object exposes translation (t / tn / to / has), Intl formatters (formatNumber / formatDate / formatRelative / formatList / displayName), locale state (setLocale / locale / locales() / dir()), translation introspection (translations(locale)), and an Express-shaped middleware() that negotiates the request locale (resolver → query → cookie → Accept-Language) and binds req.t / req.tn / req.to / req.dir / res.locals.t etc. for handlers.
Throws I18nError at boot on a malformed locale tag, a defaultLocale not present in locales, a plural-shaped entry missing other, an unknown CLDR plural key, or a missing translation file when dir is supplied without lazyLoad.
var i = b.i18n.create({
defaultLocale: "en",
locales: ["en", "es", "fr", "ja", "ar"],
translations: {
en: { greeting: "Hello, {name}!", items: { one: "{count} item", other: "{count} items" } },
es: { greeting: "Hola, {name}!" },
},
});
i.t("greeting", { name: "Alice" }); // → "Hello, Alice!"
i.tn("items", 5); // → "5 items"
i.t("greeting", { name: "Ana" }, { locale: "es" }); // → "Hola, Ana!"
i.formatNumber(1234.5, { style: "currency", currency: "USD" }); // → "$1,234.50"
i.formatRelative(-5, "minute"); // → "5 minutes ago"
i.dir({ locale: "ar" }); // → "rtl"
i.has("nav.missing"); // → false
Last updated 2026-08-08T16:39:49.652Z by seeder.