Docs

i18n

@nifrajs/i18n is framework-agnostic and dependency-free — locale negotiation plus a tiny ICU message formatter built on the platform Intl. It runs on every runtime; you bring JSON catalogs.

Negotiate the locale

negotiateLocale picks the best supported locale from a cookie (an explicit choice), then Accept-Language (quality-ranked, with fr-CAfrbase-subtag fallback), else your default. Resolve it in a loader and return just that locale's messages.

TS
// In a loader: resolve the locale + return only that catalog's messages.
import { negotiateLocale } from "@nifrajs/i18n"
import { catalogs, locales } from "../catalogs"

export async function loader({ request }) {
  const q = new URL(request.url).searchParams.get("lang")
  const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: "en", cookie: "lang" })
  return { locale, messages: catalogs[locale] }   // cookie → Accept-Language → default
}

Format messages

createFormatter(locale, messages){ t, n, d }. thandles interpolation ({name}), plural (with =N exact cases and # → the number) and select, nested — via a hand-written parser + Intl.PluralRules. n/d are memoized Intl.NumberFormat/DateTimeFormat. A missing key returns the key.

TS
// catalogs: plain JSON per locale (ICU strings). Bring your own.
export const catalogs = {
  en: { greeting: "Hello, {name}!", cart: "{count, plural, =0 {empty} one {# item} other {# items}}" },
  fr: { greeting: "Bonjour, {name} !", cart: "{count, plural, =0 {vide} one {# article} other {# articles}}" },
}

In React, provide it once and read it with useT():

TS
// The page provides the formatter; components read it with useT().
import { I18nProvider, useT } from "@nifrajs/web-react/i18n"

export default function Page({ data }) {
  return <I18nProvider locale={data.locale} messages={data.messages}><Body/></I18nProvider>
}

function Body() {
  const { t, n, d } = useT()
  return <>
    <p>{t("greeting", { name: "Ada" })}</p>
    {/* ICU plural with # substitution */}
    <p>{t("cart", { count: 3 })}</p>            {/* "3 items in your cart" */}
    <p>{t("price", { amount: n(1299.99, { style: "currency", currency: "EUR" }) })}</p>
    <p>{d(Date.now(), { dateStyle: "long" })}</p>
  </>
}

Both locale and messages are serializable, so SSR renders the negotiated catalog and the client rebuilds the same formatter on hydrate — no mismatch. Switching language re-navigates (a cookie or ?lang=); the loader returns the new catalog and the page re-renders.

Notes

  • For many locales, load catalogs lazily per request — don't bundle every catalog.
  • The supported ICU subset is interpolation + plural/select; use n()/d() for inline numbers/dates (no {n, number}skeletons). Intl.MessageFormat isn't widely available yet, so this is the portable core.
  • <I18nProvider> + useT() ship for all five adapters(React, Preact, Vue, Solid, Svelte) — import from @nifrajs/web-<framework>/i18n; each is a thin binding over the agnostic createFormatter.
Proudly built with Nifra — server-rendered on Cloudflare Pages.MIT