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-BRpt), 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) #

stable0.15.13
{
  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) #

stable0.6.0
{
  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.