Šķîþ ţö ḿàîñ çöñţéñţ

Ƒöŕḿàţţîñĝ đàţéš, ñüḿƃéŕš, çüŕŕéñçý

Ţĥéŕé àŕé ţŵö þļàçéš ƒöŕḿàţţîñĝ ĥàþþéñš, àñđ ţĥé šþļîţ ḿàţţéŕš:

  • Îñšîđé à ţŕàñšļàţéđ šţŕîñĝ: You have {n, number} unread messages. Ţĥé ŕüñţîḿé ƒöŕḿàţš ţĥé ṽàļüé ţĥŕöüĝĥ Intl, îñ ţĥé àçţîṽé ļöçàļé, àš þàŕţ öƒ ŕéšöļṽîñĝ ţĥé šţŕîñĝ. Ţĥé ţŕàñšļàţöŕ çöñţŕöļš îţ; ñö çöđé çĥàñĝé.
  • Öüţšîđé à ţŕàñšļàţéđ šţŕîñĝ: à þŕîçé îñ à ţàƃļé çéļļ, à ţîḿéšţàḿþ îñ à ļöĝ ṽîéŵ. Ţĥàţ'š ýöüŕ çöđé'š ĵöƃ. neokapi-î18ñ ĝîṽéš ýöü ţĥé ļöçàļé; ýöü ƃŕîñĝ ţĥé ƒöŕḿàţţéŕ.

Ƒöŕḿàţţîñĝ îñšîđé ţŕàñšļàţéđ šţŕîñĝš

À þļàçéĥöļđéŕ çàñ çàŕŕý àñ ÎÇÜ ƒöŕḿàţ, àñđ ţĥé ŕüñţîḿé ŕéšöļṽéš îţ àĝàîñšţ ţĥé àçţîṽé ļöçàļé:

<p>You have {count} unread messages.</p>

Ţĥé éẋţŕàçţéđ ţéḿþļàţé îš You have {count} unread messages., à ƃàŕé þļàçéĥöļđéŕ, îñţéŕþöļàţéđ ṽéŕƃàţîḿ. Ƃüţ à ţŕàñšļàţöŕ çàñ üþĝŕàđé îţ îñ ţĥé ţàŕĝéţ, ƃéçàüšé ţĥé ƒöŕḿàţ ļîṽéš îñ ţĥé šţŕîñĝ, ñöţ ţĥé šöüŕçé:

Du hast {count, number} ungelesene Nachrichten.

Ñöŵ 1234 ŕéñđéŕš àš 1.234 îñ Ĝéŕḿàñ àñđ 1,234 îñ Éñĝļîšĥ, àñđ ñöƃöđý ţöüçĥéđ ţĥé ĴŠẊ. Ţĥé šüþþöŕţéđ ƒöŕḿàţš:

ƑöŕḿàţÉẋàḿþļé îñþüţende
{n, number}1234.51,234.51.234,5
{n, number, integer}1234.51,2351.235
{n, number, percent}0.18218%18 %
{n, number, currency/EUR}1234.5€1,234.501.234,50 €
{d, date}à DateJul 11, 202611.07.2026
{d, date, short|medium|long|full}à Date7/11/2611.07.26
{t, time}à Date2:30:00 PM14:30:00
{t, time, short|medium|long|full}à Date2:30 PM14:30

Îñšîđé à <Plural> ƃŕàñçĥ, # îš ţĥé çöüñţ, ƒöŕḿàţţéđ ţĥé šàḿé ŵàý: # îñ Ĝéŕḿàñ ĝîṽéš 1.234.

Þàšš Date öƃĵéçţš àñđ ñüḿƃéŕš ţĥŕöüĝĥ àš-îš; ţĥé ŕüñţîḿé đöéš ţĥé çöñṽéŕšîöñ:

<p>Last synced {when}, {count} items.</p> // when: Date, count: number

Ţĥîš çöšţš ñö éẋţŕà ƃüñđļé: îţ'š ţĥé šàḿé Intl ţĥé þļüŕàļ ŕéšöļṽéŕ àļŕéàđý üšéš.

Ƒöŕḿàţţîñĝ öüţšîđé ţŕàñšļàţéđ šţŕîñĝš: ţĥé îñţéĝŕàţîöñ šüŕƒàçé

Éṽéŕý ļöçàļé-àŵàŕé ļîƃŕàŕý öñ ţĥé þļàţƒöŕḿ ţàķéš à ƂÇÞ-47 ļöçàļé šţŕîñĝ, ţĥé šàḿé šĥàþé neokapi-î18ñ ţŕàçķš îñţéŕñàļļý. Þüļļ îţ öüţ ŕéàçţîṽéļý ṽîà useNeokapi():

import { useNeokapi } from "@neokapi/i18n-react/runtime";

function Price({ amount, currency }: { amount: number; currency: string }) {
const { locale } = useNeokapi();
return (
<span>{new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount)}</span>
);
}

Ŵĥéñ loadTranslations() šŵàþš ţĥé đîçţ, useNeokapi() ƒîŕéš à ŕé-ŕéñđéŕ ŵîţĥ ţĥé ñéŵ ļöçàļé šţŕîñĝ. Ƒöŕḿàţţéŕš þîçķ îţ üþ öñ ţĥé ñéẋţ ŕéñđéŕ. Ýöü đöñ'ţ ñééđ ţö þļüḿƃ ļöçàļé ţĥŕöüĝĥ þŕöþš öŕ çöñţéẋţ; ţĥé ĥööķ îš ţĥé ƃöüñđàŕý.

Šţàŕţ ŵîţĥ ñàţîṽé Intl

Intl.* šĥîþš îñ éṽéŕý ḿöđéŕñ ŕüñţîḿé (ƃŕöŵšéŕ + Ñöđé ≥ 18) ŵîţĥ ƒüļļ ÇĻĐŔ đàţà. Žéŕö ƃüñđļé çöšţ, éẋçéļļéñţ ŢýþéŠçŕîþţ ţýþéš, ƒàšţ. Çöṽéŕš ţĥé çöḿḿöñ çàšéš:

// Currency
new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }).format(1234.56);
// "$1,234.56" / "1.234,56 $"

// Percent
new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(0.1824);
// "18.2%" / "18,2 %"

// Date
new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(new Date());
// "Apr 22, 2026" / "22. Apr. 2026"

// Date + time
new Intl.DateTimeFormat(locale, { dateStyle: "short", timeStyle: "short" }).format(new Date());
// "4/22/26, 6:30 PM" / "22.04.26, 18:30"

// Relative time
new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(-2, "day");
// "2 days ago" / "vor 2 Tagen"

// List
new Intl.ListFormat(locale, { style: "long", type: "conjunction" }).format([
"apples",
"oranges",
"bananas",
]);
// "apples, oranges, and bananas"

// Unit
new Intl.NumberFormat(locale, { style: "unit", unit: "kilometer-per-hour" }).format(80);
// "80 km/h"

// Compact
new Intl.NumberFormat(locale, { notation: "compact", compactDisplay: "short" }).format(1_250_000);
// "1.3M" / "1,3 Mio."

Ţĥŕéé Îñţļ ÀÞΚ ţĥàţ ḿàţţéŕ ƒöŕ öţĥéŕ šüƃšýšţéḿš:

  • Intl.PluralRules: àļŕéàđý üšéđ îñţéŕñàļļý ƃý neokapi-î18ñ'š <Plural> çöḿþöñéñţ. Ýöü đöñ'ţ ñééđ à ţĥîŕđ-þàŕţý þļüŕàļîžéŕ.
  • Intl.Collator: ļöçàļé-çöŕŕéçţ šţŕîñĝ çöḿþàŕîšöñ. Üšé ƒöŕ šöŕţîñĝ ļîšţš öƒ ţŕàñšļàţéđ ñàḿéš (items.sort((a, b) => new Intl.Collator(locale).compare(a.name, b.name))).
  • Intl.Segmenter: ŵöŕđ / šéñţéñçé / ĝŕàþĥéḿé ƃöüñđàŕîéš (üšéƒüļ ŵĥéñ ýöü ŵàñţ ţö çüţ à ļàƃéļ ḿîđ-ŵöŕđ çöŕŕéçţļý îñ ÇĴĶ).

Ŕéüšàƃļé ƒöŕḿàţţéŕ ĥööķš

Ŕé-çŕéàţîñĝ ƒöŕḿàţţéŕš öñ éṽéŕý ŕéñđéŕ îš ƒîñé (ţĥéý'ŕé çĥéàþ), ƃüţ ḿéḿöîžîñĝ îš çļéàñéŕ, àñđ ļéţš ýöü šĥàŕé çöñƒîĝüŕàţîöñ àçŕöšš çöḿþöñéñţš. À ţîñý ŵŕàþþéŕ:

import { useMemo } from "react";
import { useNeokapi } from "@neokapi/i18n-react/runtime";

export function useCurrency(currency: string) {
const { locale } = useNeokapi();
return useMemo(
() => new Intl.NumberFormat(locale, { style: "currency", currency }),
[locale, currency],
);
}

export function useDateFormat(options: Intl.DateTimeFormatOptions = { dateStyle: "medium" }) {
const { locale } = useNeokapi();
// Stringify options once so useMemo deps stay stable for callers
// passing a fresh object literal each render.
const key = JSON.stringify(options);
return useMemo(() => new Intl.DateTimeFormat(locale, options), [locale, key]);
}

Üšàĝé:

function Cart() {
const currency = useCurrency("EUR");
const date = useDateFormat({ dateStyle: "short" });
return (
<footer>
Subtotal: {currency.format(subtotal)}
Delivered: {date.format(deliveryDate)}
</footer>
);
}

Ţĥîŕđ-þàŕţý ļîƃŕàŕîéš

Ŵĥéñ Îñţļ đöéšñ'ţ çöṽéŕ ŵĥàţ ýöü ñééđ, ţĥé îđîöḿ îš ţĥé šàḿé: const { locale } = useNeokapi() → ḿàþ ţö ţĥé ļîƃŕàŕý'š ļöçàļé ţýþé → þàšš îñ.

đàţé-ƒñš

đàţé-ƒñš ļöçàļéš àŕé éẋþļîçîţ îḿþöŕţš. Ķééþ à šḿàļļ ḿàþ:

import { formatDistance, format } from "date-fns";
import { enUS, de, fr, es, ja } from "date-fns/locale";
import type { Locale } from "date-fns";

const DATE_FNS_LOCALES: Record<string, Locale> = {
en: enUS,
de,
fr,
es,
ja,
};

function useDateFnsLocale(): Locale {
const { locale } = useNeokapi();
const primary = locale.split("-")[0];
return DATE_FNS_LOCALES[primary] ?? enUS;
}

function Ago({ date }: { date: Date }) {
const dfl = useDateFnsLocale();
return <time>{formatDistance(date, new Date(), { addSuffix: true, locale: dfl })}</time>;
}

Ƒöŕ ţîḿéžöñé-àŵàŕé ƒöŕḿàţţîñĝ, þàîŕ ŵîţĥ date-fns-tz.

Ļüẋöñ

Ļüẋöñ šþéàķš ƂÇÞ-47 ñàţîṽéļý (îţ'š àñ Îñţļ ŵŕàþþéŕ üñđéŕñéàţĥ) àñđ šüþþöŕţš ţîḿéžöñéš ƒîŕšţ-çļàšš.

import { DateTime } from "luxon";

function LocalTime({ iso, zone }: { iso: string; zone: string }) {
const { locale } = useNeokapi();
const dt = DateTime.fromISO(iso, { zone }).setLocale(locale);
return <time>{dt.toLocaleString(DateTime.DATETIME_MED)}</time>;
}

đàýĵš

import dayjs from "dayjs";
import "dayjs/locale/de";
import "dayjs/locale/fr";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);

function Ago({ iso }: { iso: string }) {
const { locale } = useNeokapi();
return <time>{dayjs(iso).locale(locale).fromNow()}</time>;
}

Šîđé-郃éçţ îḿþöŕţš ŕéĝîšţéŕ ļöçàļé đàţà; öñé þéŕ ļöçàļé ýöü šĥîþ.

ƑöŕḿàţĴŠ / ŕéàçţ-îñţļ

ƑöŕḿàţĴŠ îš à ƒüļļ-ƒéàţüŕéđ ÎÇÜ ḾéššàĝéƑöŕḿàţ šţàçķ. ΃ ýöü'ŕé àļŕéàđý öñ îţ, neokapi-î18ñ àñđ ƑöŕḿàţĴŠ çàñ çöéẋîšţ: üšé ƑöŕḿàţĴŠ ƒöŕ ƒöŕḿàţţîñĝ àñđ neokapi-î18ñ ƒöŕ éẋţŕàçţîöñ + ţŕàñšļàţîöñ. Ƃüţ ýöü'ļļ ĥàṽé ţŵö šýšţéḿš ţŕàçķîñĝ ļöçàļé: ŵîŕé currentLocale îñţö ƑöŕḿàţĴŠ'š IntlProvider:

import { IntlProvider } from "react-intl";
import { useNeokapi } from "@neokapi/i18n-react/runtime";

function I18nRoot({ children }) {
const { locale } = useNeokapi();
return (
<IntlProvider locale={locale} defaultLocale="en" messages={{}}>
{children}
</IntlProvider>
);
}

Ƒöŕ ĝŕééñƒîéļđ àþþš: šţîçķ ŵîţĥ Îñţļ. ƑöŕḿàţĴŠ šĥîþš îţš öŵñ ŕüñţîḿé ƒöŕ ƒéàţüŕéš neokapi-î18ñ àļŕéàđý ĥàñđļéš (þļüŕàļš, šéļéçţ, ḿéššàĝé îñţéŕþöļàţîöñ, àñđ ñüḿƃéŕ/đàţé/ţîḿé ƒöŕḿàţţîñĝ îñšîđé šţŕîñĝš) þļüš šöḿé îţ đöéšñ'ţ (ŵĥîçĥ Îñţļ öƒţéñ çöṽéŕš).

Ļîƃŕàŕý þîçķéŕ

ÑééđÞîçķÑöţéš
Çüŕŕéñçý, þéŕçéñţ, đàţé, ţîḿé, ŕéļàţîṽé ţîḿé, ļîšţ, üñîţ, çöḿþàçţ ñüḿƃéŕšIntl.*Àļŕéàđý îñ ţĥé ŕüñţîḿé. Ñö îḿþöŕţš.
Þļüŕàļîžàţîöñ (çöüñţ-àŵàŕé çöþý)neokapi-î18ñ'š <Plural>Üšéš Intl.PluralRules. Ñö éẋţŕà ļîƃŕàŕý.
Šöŕţîñĝ ţŕàñšļàţéđ ñàḿéšIntl.Collatorlist.sort((a,b) => col.compare(a,b))
Ţîḿéžöñé-àŵàŕé đàţéš, ĥéàṽý đàţé ḿàţĥĻüẋöñ öŕ đàţé-ƒñš(-ţž)Ļüẋöñ îš Îñţļ-ƃàšéđ; đàţé-ƒñš îḿþöŕţš þéŕ ƒüñçţîöñ.
Đüŕàţîöñ ƒöŕḿàţţîñĝ ("3ĥ 12ḿ")Ļüẋöñ Duration.toHuman() öŕ @formatjs/intl-durationformat þöļýƒîļļIntl.DurationFormat éẋîšţš îñ ñéŵéŕ ŕüñţîḿéš ƃüţ îšñ'ţ üñîṽéŕšàļļý šĥîþþéđ ýéţ.
Ļéĝàçý ḿöḿéñţ.ĵš çöđéƃàšéḿîĝŕàţé îñçŕéḿéñţàļļýḿöḿéñţ.ĵš îš îñ ḿàîñţéñàñçé ḿöđé; Ļüẋöñ àñđ đàţé-ƒñš àŕé ţĥé üšüàļ ḿîĝŕàţîöñ ţàŕĝéţš.
Ñüḿƃéŕ/đàţé/ţîḿé îñšîđé à ţŕàñšļàţéđ šţŕîñĝÎÇÜ îñ ţĥé šţŕîñĝ: {n, number}, {d, date, long}Ƃüîļţ îñ; šéé ţĥé ţöþ öƒ ţĥîš þàĝé. Ñö ļîƃŕàŕý.
ÎÇÜ ḾéššàĝéƑöŕḿàţ ƃéýöñđ ţĥàţ šüƃšéţ (öŕđîñàļš, üñîţ šķéļéţöñš, …)@formatjs/intl-messageformat šţàñđàļöñéĴüšţ ţĥé ƒöŕḿàţţéŕ, ñöţ ţĥé ŵĥöļé ŕéàçţ-îñţļ šţàçķ.

Îñîţîàļ ŕéñđéŕ àñđ ŠŠŔ

Àļļ öƒ ţĥé àƃöṽé ŕéàđ ţĥé ļöçàļé àţ ŕéñđéŕ ţîḿé. Öñ ƒîŕšţ þàîñţ (ƃéƒöŕé loadTranslations() ŕéšöļṽéš), useNeokapi() ŕéţüŕñš ţĥé đéƒàüļţ ļöçàļé ("" üñļéšš ýöü þŕé-çàļļéđ setTranslations). Ţĥàţ üšüàļļý ḿàþš ţö Éñĝļîšĥ ƒàļļƃàçķ ƒöŕḿàţţîñĝ, ŵĥîçĥ ḿàţçĥéš ţĥé Éñĝļîšĥ šöüŕçé ţéẋţ ţĥé àþþ ŕéñđéŕš ƃéƒöŕé ţŕàñšļàţîöñš àŕŕîṽé. ΃ ţĥé ƒļîçķéŕ ḿàţţéŕš, šééđ ţĥé ļöçàļé öñ ţĥé šéŕṽéŕ šîđé:

// On the server, before hydration
setTranslations(cookieLocale, {}); // empty dict; locale alone is enough

Ñöŵ ţĥé ƒîŕšţ çļîéñţ ŕéñđéŕ ĥàþþéñš ŵîţĥ ţĥé ŕîĝĥţ ļöçàļé, Îñţļ ƒöŕḿàţţéŕš ḿàţçĥ, àñđ ţĥé đîçţ šŵàþ çĥàñĝéš šţŕîñĝš öñļý àñđ ļéàṽéš ƒöŕḿàţţîñĝ àļöñé.

Šéé àļšö Çöñƒîĝüŕàţîöñ → ĤŢḾĻ lang àñđ dir àţţŕîƃüţéš ƒöŕ ķééþîñĝ ţĥé đöçüḿéñţ ļöçàļé îñ šýñç öñ ƒîŕšţ þàîñţ.

Ŵĥàţ neokapi-î18ñ đéļîƃéŕàţéļý đöéšñ'ţ đö

  • Ñüḿƃéŕ îñþüţ þàŕšîñĝ. Þàŕšîñĝ "1.234,56 €" ƃàçķ îñţö 1234.56 îš ļöçàļé-đéþéñđéñţ àñđ ñöñ-ţŕîṽîàļ. Üšé à ƒöŕḿ ļîƃŕàŕý ŵîţĥ à ļöçàļé-àŵàŕé îñþüţ (react-number-format ĥàš ļöçàļé šüþþöŕţ) öŕ ŵŕîţé à šḿàļļ þàŕšéŕ þéŕ îñþüţ šĥàþé.
  • Üñîţ çöñṽéŕšîöñ. Îñţļ ƒöŕḿàţš "1 ķḿ"; çöñṽéŕţîñĝ 1 ķḿ ţö ḿîļéš îš ýöüŕ àþþ'š ŕéšþöñšîƃîļîţý.
  • Àđđŕéšš / þĥöñé / þöšţàļ çöđé ƒöŕḿàţţîñĝ. Üšé à šþéçîàļîžéđ ļîƃŕàŕý (libphonenumber-js, libpostal).

Ţĥéšé àŕéñ'ţ î18ñ çöñçéŕñš šö ḿüçĥ àš đàţà ñöŕḿàļîžàţîöñ: ţĥéý ñééđ đöḿàîñ ļöĝîç neokapi-î18ñ ĥàš ñö ƃüšîñéšš îñ.

Ñéẋţ

  • Þļüŕàļš àñđ šéļéçţ: ţĥé öñé ƒöŕḿàţţîñĝ çàšé neokapi-î18ñ đöéš öŵñ, ƃéçàüšé îţ'š îñţéŕţŵîñéđ ŵîţĥ ţĥé ţŕàñšļàţéđ šţŕîñĝ îţšéļƒ.
  • t() éšçàþé ĥàţçĥ: ƒééđîñĝ ƒöŕḿàţţéđ ṽàļüéš îñţö ţŕàñšļàţéđ çöþý ṽîà þļàçéĥöļđéŕš: t("Price: {price}", { price: currencyFormatter.format(amount) }).
  • Çöñƒîĝüŕàţîöñ: ŕüñţîḿé öþţîöñš, îñçļüđîñĝ ţĥé <html lang> / dir šýñç.