@coherent.js/i18n

Internationalization for Coherent.js applications: translations with interpolation and pluralization, locale-aware formatters, and locale management.

pnpm add @coherent.js/i18n

Translations

Create a translator, add messages per locale, and translate with t(). Interpolation uses {{name}} placeholders by default:

import { createTranslator } from '@coherent.js/i18n';

const translator = createTranslator({ defaultLocale: 'en', fallbackLocale: 'en' });

translator.addTranslations('en', {
  common: {
    welcome: 'Welcome',
    hello: 'Hello, {{name}}!'
  }
});
translator.addTranslations('fr', {
  common: {
    welcome: 'Bienvenue',
    hello: 'Bonjour, {{name}} !'
  }
});

translator.t('common.hello', { name: 'Ada' }); // "Hello, Ada!"
translator.setLocale('fr');
translator.t('common.welcome');                // "Bienvenue"

Keys are dot-paths into the translation object; only the translations' own properties are looked up, so t('constructor') is a missing key like any other. Missing keys fall back to fallbackLocale, then to the key itself (customizable via missingKeyHandler).

Locales resolve to the closest loaded one: after setLocale('fr-FR') with only fr loaded, getLocale() is 'fr', and a key missing from fr-CA is looked up in fr before the fallback locale. Params are inserted literally (a $ or a {{placeholder}} inside a value is not expanded).

Server-side rendering: one locale per request

setLocale() changes the translator's current locale for everyone using it. On a server, bind a translator to each request with forLocale(), which never touches the shared locale:

const i18n = createTranslator({ defaultLocale: 'en', fallbackLocale: 'en' });
// ...addTranslations() once at startup

app.get('/', (req, res) => {
  const { t, locale } = i18n.forLocale(req.acceptsLanguages('en', 'fr') || 'en');
  res.send(render({ html: { lang: locale, children: [{ body: { children: [{ p: { text: t('common.welcome') } }] } }] } }));
});

forLocale(locale, { escape }) returns { locale, t, has, getLocale }.

Escaping params

text: is escaped by core, so it is the safe place for translations. When a translation contains markup and goes through html:, escape the params (the template itself is trusted):

translator.addTranslations('en', { joined: '<strong>{{name}}</strong> joined' });

{ p: { html: translator.t('joined', { name: userInput }, { escape: true }) } }

createTranslator({ escape: true }) escapes params in every call; the third argument of t() is a locale string or { locale, escape }.

Scoped translators

createScopedTranslator(translator, namespace) prefixes every key, which keeps component code short:

import { createScopedTranslator } from '@coherent.js/i18n';

const { t } = createScopedTranslator(translator, 'common'); // { t, has, getLocale, setLocale }
t('hello', { name: 'Ada' }); // same as translator.t('common.hello', ...)

Formatters

Locale-aware date, number, currency, and list formatting built on Intl:

import { createFormatters } from '@coherent.js/i18n';

const fmt = createFormatters('en-US');

fmt.date.format(new Date());   // "1/15/2026"
fmt.number.format(12345.678);  // "12,345.678"
fmt.currency.format(1999.99);  // "$1,999.99"

Individual formatter classes (DateFormatter, NumberFormatter, CurrencyFormatter, ListFormatter) are also exported. fmt.date.relative(date) picks the largest fitting unit in either direction ("in 2 days", "last week", "3 months ago").

Locale management

import {
  createLocaleManager,
  detectLocale,
  normalizeLocale,
  isRTL,
  getLocaleDirection
} from '@coherent.js/i18n';

const locales = createLocaleManager({ defaultLocale: 'en-US' });
locales.getLocale();          // current locale

detectLocale();               // best-guess locale for the environment
normalizeLocale('EN_us');     // "en" (pass true as second argument to keep the region: "en-us")
isRTL('ar');                  // true
getLocaleDirection('he');     // "rtl"

Using translations in components

Components are plain functions, so pass the translator (or a scoped t) like any other dependency:

export function Greeting({ t, name }) {
  return { p: { text: t('hello', { name }) } };
}

See also