Languages
Languages other than English, slotsmith’s default, come as locale packs. For another language, import its locale pack and pass it as locale to a component, or to SlotsmithProvider for a whole app. A pack translates every string the components render and writes numbers and plurals the way its language does.
Available packs
slotsmith ships one pack per language below. Each is its own entry, slotsmith/locales/<code>, so an app bundles only the languages it imports. This table is read from the library’s src/locales/ folder when the site is built, so it lists exactly what ships.
| Language | Import | Export | Direction |
|---|---|---|---|
| Arabic | slotsmith/locales/ar | ar | rtl |
| Arabic (Egypt) | slotsmith/locales/ar-EG | arEG | rtl |
| Arabic (Iraq) | slotsmith/locales/ar-IQ | arIQ | rtl |
| Arabic (Saudi Arabia) | slotsmith/locales/ar-SA | arSA | rtl |
| German | slotsmith/locales/de | de | ltr |
| Spanish | slotsmith/locales/es | es | ltr |
| Persian | slotsmith/locales/fa | fa | rtl |
| French | slotsmith/locales/fr | fr | ltr |
| Hebrew | slotsmith/locales/he | he | rtl |
| Hindi | slotsmith/locales/hi | hi | ltr |
| Indonesian | slotsmith/locales/id | id | ltr |
| Italian | slotsmith/locales/it | it | ltr |
| Japanese | slotsmith/locales/ja | ja | ltr |
| Korean | slotsmith/locales/ko | ko | ltr |
| Brazilian Portuguese | slotsmith/locales/pt-BR | ptBR | ltr |
| Russian | slotsmith/locales/ru | ru | ltr |
| Turkish | slotsmith/locales/tr | tr | ltr |
| Chinese (China) | slotsmith/locales/zh-CN | zhCN | ltr |
There is no en pack: English is every component’s default. The regional Arabic packs (ar-EG, ar-IQ, ar-SA) reuse the ar text and write numbers the way their region does. The packs other than Arabic were drafted from the English labels; corrections from native speakers are welcome on the repository’s issues.
Try it
Pick a language. The table, the autocomplete, the date picker and the uploader switch together, because they sit inside one SlotsmithProvider handed that pack. For the right-to-left packs the stage also flips to dir="rtl". Only the components’ own strings change: the rows and the column headers are your data, in your language.
| Lena Park | Engineer | Platform | |
| Omar Haddad | Designer | Product | |
| Sofia Reyes | Engineer | Payments |
import { useState } from "react";
import { Autocomplete, type OptionValue } from "slotsmith/autocomplete";
import { DataTable, type DataTableColumnDef } from "slotsmith/data-table";
import { DatePicker } from "slotsmith/date-picker";
import { FileUploader } from "slotsmith/file-uploader";
import type { SlotsmithLocale } from "slotsmith/locale";
import { SlotsmithProvider } from "slotsmith/provider";
import { ar } from "slotsmith/locales/ar";
import { arEG } from "slotsmith/locales/ar-EG";
import { arIQ } from "slotsmith/locales/ar-IQ";
import { arSA } from "slotsmith/locales/ar-SA";
import { de } from "slotsmith/locales/de";
import { es } from "slotsmith/locales/es";
import { fa } from "slotsmith/locales/fa";
import { fr } from "slotsmith/locales/fr";
import { he } from "slotsmith/locales/he";
import { hi } from "slotsmith/locales/hi";
import { id } from "slotsmith/locales/id";
import { it } from "slotsmith/locales/it";
import { ja } from "slotsmith/locales/ja";
import { ko } from "slotsmith/locales/ko";
import { ptBR } from "slotsmith/locales/pt-BR";
import { ru } from "slotsmith/locales/ru";
import { tr } from "slotsmith/locales/tr";
import { zhCN } from "slotsmith/locales/zh-CN";
import { people, type Person } from "../shared/people";
import { topics } from "../shared/options";
// Each language named in itself, the way a language menu should name it.
const LANGUAGES: { pack: SlotsmithLocale | null; name: string }[] = [
{ pack: null, name: "English" },
{ pack: ar, name: "العربية" },
{ pack: arEG, name: "العربية (مصر)" },
{ pack: arIQ, name: "العربية (العراق)" },
{ pack: arSA, name: "العربية (السعودية)" },
{ pack: de, name: "Deutsch" },
{ pack: es, name: "Español" },
{ pack: fa, name: "فارسی" },
{ pack: fr, name: "Français" },
{ pack: he, name: "עברית" },
{ pack: hi, name: "हिन्दी" },
{ pack: id, name: "Bahasa Indonesia" },
{ pack: it, name: "Italiano" },
{ pack: ja, name: "日本語" },
{ pack: ko, name: "한국어" },
{ pack: ptBR, name: "Português (Brasil)" },
{ pack: ru, name: "Русский" },
{ pack: tr, name: "Türkçe" },
{ pack: zhCN, name: "简体中文" },
];
const rows = people.slice(0, 6);
const columns: DataTableColumnDef<Person>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "role", header: "Role" },
{ accessorKey: "team", header: "Team" },
];
export default function Switcher() {
const [pack, setPack] = useState<SlotsmithLocale | null>(null);
const [topicIds, setTopicIds] = useState<OptionValue[]>(["accessibility", "forms"]);
// English needs no pack: it is every component's default.
const code = pack?.code ?? "en";
const dir = pack?.dir ?? "ltr";
return (
<div className="languages-switcher">
{/* Buttons on a wide screen, a native menu on a narrow one (languages.css switches them). */}
<div role="group" aria-label="Language of the demo">
{LANGUAGES.map((language) => (
<button
key={language.pack?.code ?? "en"}
type="button"
lang={language.pack?.code ?? "en"}
dir="auto"
aria-pressed={language.pack === pack}
onClick={() => setPack(language.pack)}
>
{language.name}
</button>
))}
</div>
<select
className="languages-switcher__select"
aria-label="Language of the demo"
value={code}
onChange={(event) => setPack(LANGUAGES.find((language) => (language.pack?.code ?? "en") === event.target.value)!.pack)}
>
{LANGUAGES.map((language) => (
<option key={language.pack?.code ?? "en"} value={language.pack?.code ?? "en"} lang={language.pack?.code ?? "en"}>
{language.name}
</option>
))}
</select>
{/* The provider renders no element: the direction goes on an element of your own. */}
<SlotsmithProvider locale={pack ?? "en-US"}>
<div data-stage dir={dir} lang={code}>
<DataTable
data={rows}
columns={columns}
enableRowSelection
defaultPagination={{ pageIndex: 0, pageSize: 3 }}
pageSizeOptions={[3, 6]}
/>
<div className="languages-switcher__fields">
<Autocomplete multiple options={topics} value={topicIds} onChange={setTopicIds} maxTags={1} aria-label="Topics" />
<DatePicker mode="range" aria-label="Stay" />
</div>
<FileUploader multiple accept="image/*" maxSize={2 * 1024 * 1024} maxFiles={3} />
</div>
</SlotsmithProvider>
</div>
);
}import type { DataTableColumnDef } from "slotsmith/data-table";
export type Person = { id: string; name: string; role: string; team: string; status: string; joined: string };
export const people: Person[] = [
{ id: "1", name: "Lena Park", role: "Engineer", team: "Platform", status: "Active", joined: "2021-03-08" },
{ id: "2", name: "Omar Haddad", role: "Designer", team: "Product", status: "Active", joined: "2020-11-16" },
{ id: "3", name: "Sofia Reyes", role: "Engineer", team: "Payments", status: "Away", joined: "2022-01-24" },
{ id: "4", name: "Yuki Tanaka", role: "Manager", team: "Platform", status: "Active", joined: "2019-06-03" },
{ id: "5", name: "Amir Nasser", role: "Analyst", team: "Growth", status: "Active", joined: "2023-02-13" },
{ id: "6", name: "Chloe Martin", role: "Engineer", team: "Payments", status: "On leave", joined: "2021-09-27" },
{ id: "7", name: "Jonas Weber", role: "Support", team: "Care", status: "Active", joined: "2022-07-11" },
{ id: "8", name: "Priya Nair", role: "Designer", team: "Product", status: "Away", joined: "2020-04-20" },
{ id: "9", name: "Diego Alvarez", role: "Engineer", team: "Growth", status: "Active", joined: "2023-05-30" },
{ id: "10", name: "Hana Kim", role: "Manager", team: "Care", status: "Active", joined: "2018-10-01" },
{ id: "11", name: "Tomas Novak", role: "Analyst", team: "Payments", status: "On leave", joined: "2022-12-05" },
{ id: "12", name: "Nadia Farouk", role: "Support", team: "Care", status: "Active", joined: "2024-01-15" },
];
export const columns: DataTableColumnDef<Person>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "role", header: "Role" },
{ accessorKey: "team", header: "Team" },
{ accessorKey: "status", header: "Status" },
{ accessorKey: "joined", header: "Joined" },
];export type Country = { code: string; name: string };
/** Sorted by name, as a picker shows them. */
export const countries: Country[] = [
{ code: "ar", name: "Argentina" },
{ code: "au", name: "Australia" },
{ code: "br", name: "Brazil" },
{ code: "ca", name: "Canada" },
{ code: "eg", name: "Egypt" },
{ code: "fr", name: "France" },
{ code: "de", name: "Germany" },
{ code: "in", name: "India" },
{ code: "jp", name: "Japan" },
{ code: "jo", name: "Jordan" },
{ code: "ke", name: "Kenya" },
{ code: "mx", name: "Mexico" },
{ code: "ma", name: "Morocco" },
{ code: "nl", name: "Netherlands" },
{ code: "no", name: "Norway" },
{ code: "pt", name: "Portugal" },
{ code: "sa", name: "Saudi Arabia" },
{ code: "za", name: "South Africa" },
{ code: "es", name: "Spain" },
{ code: "se", name: "Sweden" },
{ code: "tr", name: "Türkiye" },
{ code: "ae", name: "United Arab Emirates" },
{ code: "gb", name: "United Kingdom" },
{ code: "us", name: "United States" },
];
/** `id` and `label` are the names the autocomplete reads by default, so topics need no getters. */
export type Topic = { id: string; label: string; archived?: boolean };
export const topics: Topic[] = [
{ id: "accessibility", label: "Accessibility" },
{ id: "animation", label: "Animation" },
{ id: "caching", label: "Caching" },
{ id: "design-systems", label: "Design systems" },
{ id: "forms", label: "Forms" },
{ id: "i18n", label: "Internationalisation" },
{ id: "performance", label: "Performance" },
{ id: "rtl", label: "Right-to-left" },
{ id: "server-components", label: "Server components" },
{ id: "state", label: "State" },
{ id: "testing", label: "Testing" },
{ id: "typescript", label: "TypeScript" },
{ id: "legacy", label: "Legacy browsers", archived: true },
];
/** A country with the region it is listed under; a `disabled` one stays in the list but cannot be picked. */
export type RegionCountry = Country & { region: string; disabled?: boolean };
/** A short list grouped by region. Groups show in the order of their first option, so the regions come in this order. */
export const countriesByRegion: RegionCountry[] = [
{ code: "de", name: "Germany", region: "Europe" },
{ code: "fr", name: "France", region: "Europe" },
{ code: "es", name: "Spain", region: "Europe" },
{ code: "ge", name: "Georgia", region: "Europe" },
{ code: "dz", name: "Algeria", region: "Africa" },
{ code: "eg", name: "Egypt", region: "Africa" },
{ code: "ng", name: "Nigeria", region: "Africa" },
{ code: "ne", name: "Niger", region: "Africa", disabled: true },
{ code: "jp", name: "Japan", region: "Asia" },
{ code: "sg", name: "Singapore", region: "Asia" },
{ code: "sa", name: "Saudi Arabia", region: "Asia" },
{ code: "sy", name: "Syria", region: "Asia" },
];The strings you cannot see change too: focus a row’s checkbox or the uploader and a screen reader reads its name from the pack.
Passing a locale
Every component takes locale. Hand it a pack as an object, and that component uses it, with or without a provider:
import { DatePicker } from "slotsmith/date-picker";
import { ar } from "slotsmith/locales/ar";
// A pack passed as an object: no provider needed, and it covers this picker only.
export default function ObjectForm() {
return (
<div dir={ar.dir} lang={ar.code}>
<DatePicker locale={ar} aria-label="تاريخ التسليم" />
</div>
);
}For a whole app, give SlotsmithProvider the active tag and the packs it may resolve to. SlotsmithProvider and useSlotsmithLocale come from slotsmith/provider. The same provider also takes components, which replaces slots for every component below it.
import type { ReactNode } from "react";
import { SlotsmithProvider, useSlotsmithLocale } from "slotsmith/provider";
import { ar } from "slotsmith/locales/ar";
import { fr } from "slotsmith/locales/fr";
// The provider renders no element, so the direction goes on your own markup.
function Page({ children }: { children: ReactNode }) {
const { code, dir } = useSlotsmithLocale();
return (
<main lang={code} dir={dir}>
{children}
</main>
);
}
// A tag resolves against the packs in `locales`. "ar-EG" has no pack of its
// own here, so it uses ar and writes numbers and dates the Egyptian way.
// English is the default and needs no pack.
export function App({ language, children }: { language: "en-US" | "ar-EG" | "fr"; children: ReactNode }) {
return (
<SlotsmithProvider locale={language} locales={[ar, fr]}>
<Page>{children}</Page>
</SlotsmithProvider>
);
}A tag finds its pack in this order: the exact tag (ar-EG finds an ar-EG pack), then the bare language (ar-EG finds ar), then any pack of the same language. The tag is still what Intl formats with, so ar-EG resolved to the ar pack writes Egyptian digits. With no match the labels stay English, numbers and dates follow the tag, and the console warns once per tag during development. A tag whose language is en never warns.
The provider renders no element, so it cannot set the direction for you. useSlotsmithLocale() reads the active tag and direction back (en-US and ltr outside a provider) for the element that should carry lang and dir.
Your own pack
defineLocale from slotsmith/locale types a custom locale and fills in its direction from the tag. A locale has one section per component: table, autocomplete, datePicker, fileUploader, and fileValidation for the uploader’s rejection messages. Every section is optional, but one that is present must have every key, so the type checker refuses a half-finished translation.
| Amsterdam | Noord-Holland | |
| Rotterdam | Zuid-Holland |
import { DataTable, type DataTableColumnDef } from "slotsmith/data-table";
import { createNumber, defineLocale } from "slotsmith/locale";
// The direction is inferred from the code: "ltr" here, "rtl" for ar, fa, he, ur…
// Every section is optional, but one that is present must have every key:
// leaving one out is a type error, not an English word in a Dutch screen.
export const nl = defineLocale({
code: "nl",
// A section can be a function of the active tag, so numbers follow it.
table: (code) => {
const number = createNumber(code);
return {
empty: "Geen gegevens",
error: "Er ging iets mis bij het laden.",
retry: "Opnieuw proberen",
rowsPerPage: "Rijen per pagina",
pageInfo: (page, pageCount) => `Pagina ${number(page)} van ${number(pageCount)}`,
pagination: "Paginering",
previousPage: "Vorige pagina",
nextPage: "Volgende pagina",
selectAll: "Alle rijen op deze pagina selecteren",
selectRow: "Rij selecteren",
expandRow: "Rij uitklappen",
collapseRow: "Rij inklappen",
reorderRow: "Rij verplaatsen",
reorderInstructions:
"Druk op spatie om de rij op te pakken, gebruik de pijltjestoetsen om te verplaatsen, spatie om neer te zetten en Escape om te annuleren.",
reorderLifted: (position, total) => `Rij opgepakt. Positie ${number(position)} van ${number(total)}.`,
reorderMoved: (position, total) => `Positie ${number(position)} van ${number(total)}.`,
reorderDropped: (position, total) => `Rij neergezet op positie ${number(position)} van ${number(total)}.`,
reorderCancelled: "Verplaatsen geannuleerd.",
reorderLevel: (level) => `Niveau ${number(level)}.`,
editCell: (column) => `${column} bewerken`,
invalidNumber: "Voer een getal in.",
invalidOption: "Kies een van de opties.",
invalidDate: "Voer een geldige datum in.",
invalidBoolean: "Voer TRUE of FALSE in.",
cellsCopied: (count) => `Cellen gekopieerd: ${number(count)}.`,
cellsCut: (count) => `Cellen geknipt: ${number(count)}.`,
cellsPasted: (count, skipped) =>
skipped > 0
? `Cellen geplakt: ${number(count)}, overgeslagen: ${number(skipped)}.`
: `Cellen geplakt: ${number(count)}.`,
cellsCleared: (count) => `Cellen gewist: ${number(count)}.`,
cellsFilled: (count) => `Cellen gevuld: ${number(count)}.`,
undone: "Ongedaan gemaakt.",
redone: "Opnieuw gedaan.",
};
},
// autocomplete, datePicker, fileUploader and fileValidation are left out:
// those components stay in English.
});
type City = { id: string; name: string; province: string };
const cities: City[] = [
{ id: "1", name: "Amsterdam", province: "Noord-Holland" },
{ id: "2", name: "Rotterdam", province: "Zuid-Holland" },
{ id: "3", name: "Utrecht", province: "Utrecht" },
{ id: "4", name: "Groningen", province: "Groningen" },
{ id: "5", name: "Maastricht", province: "Limburg" },
];
const columns: DataTableColumnDef<City>[] = [
{ accessorKey: "name", header: "Stad" },
{ accessorKey: "province", header: "Provincie" },
];
export default function CustomPack() {
return (
<div lang="nl">
<DataTable locale={nl} data={cities} columns={columns} enableRowSelection defaultPagination={{ pageIndex: 0, pageSize: 2 }} pageSizeOptions={[2, 5]} />
</div>
);
}A locale written in plain JavaScript, or one that predates a newer key, is still safe: the missing string falls back to English and nothing throws. Register a custom locale with the provider like any pack, locales={[ar, nl]}, or hand it straight to a component.
Overriding labels
The labels prop replaces single strings on one component and keeps the rest from its locale:
import { DataTable, type DataTableColumnDef } from "slotsmith/data-table";
import { ar } from "slotsmith/locales/ar";
type Order = { id: string; customer: string };
const columns: DataTableColumnDef<Order>[] = [{ accessorKey: "customer", header: "العميل" }];
// Every string from the Arabic pack, except `empty`, replaced for this table only.
export default function Override() {
return (
<div dir="rtl" lang="ar">
<DataTable locale={ar} labels={{ empty: "لا توجد طلبات بعد" }} data={[]} columns={columns} />
</div>
);
}Each component’s labels and their English defaults are listed on its API page, for example the data table’s labels.
Precedence
Every string is resolved on its own. Each step overrides the one before it:
- The English default.
- The
localeof the nearestSlotsmithProvider. - The component’s own
locale. It replaces the provider’s whole, so one component never mixes two languages. - The component’s
labelsprop.
import { DatePicker } from "slotsmith/date-picker";
import { SlotsmithProvider } from "slotsmith/provider";
import { ar } from "slotsmith/locales/ar";
import { fr } from "slotsmith/locales/fr";
// Each string is resolved on its own: the English default, then the
// provider's locale, then the component's own locale (which replaces the
// provider's whole), then the `labels` prop. The pickers use the button
// trigger, which shows the placeholder label.
export default function Precedence() {
return (
<SlotsmithProvider locale="fr" locales={[ar, fr]}>
<div style={{ display: "grid", gap: "0.75rem", justifyItems: "start" }}>
{/* French, from the provider. */}
<DatePicker trigger="button" aria-label="Date de départ" />
<div dir="rtl" style={{ display: "grid", gap: "0.75rem", justifyItems: "start" }}>
{/* Arabic: the component's own locale replaces the provider's. */}
<DatePicker trigger="button" locale={ar} aria-label="تاريخ المغادرة" />
{/* Arabic, except the one string `labels` replaces. */}
<DatePicker trigger="button" locale={ar} labels={{ placeholder: "تاريخ الوصول" }} aria-label="تاريخ الوصول" />
</div>
</div>
</SlotsmithProvider>
);
}Plurals
createPlural(code) from slotsmith/locale picks the form a count needs through Intl.PluralRules and writes the formatted count into {count}. Give one string per category the language uses; only other is required. Russian has three forms for whole numbers and Arabic has six. Pick a count:
import { useState } from "react";
import { createPlural, defineLocale, type PluralForms } from "slotsmith/locale";
// One string per plural category the language uses; `{count}` is replaced by
// the number, written the way the tag writes numbers. Only `other` is required.
const SELECTED: Record<string, { name: string; forms: PluralForms }> = {
en: {
name: "English",
forms: { one: "{count} row selected", other: "{count} rows selected" },
},
ru: {
name: "Русский",
forms: {
one: "Выбрана {count} строка",
few: "Выбраны {count} строки",
many: "Выбрано {count} строк",
other: "Выбрано {count} строки",
},
},
ar: {
name: "العربية",
forms: {
zero: "لم يُحدَّد أي صف",
one: "تم تحديد صف واحد",
two: "تم تحديد صفّين",
few: "تم تحديد {count} صفوف",
many: "تم تحديد {count} صفًّا",
other: "تم تحديد {count} صف",
},
},
};
const COUNTS = [0, 1, 2, 5, 11, 100];
export default function Plurals() {
const [code, setCode] = useState("ru");
const [count, setCount] = useState(2);
// createPlural picks the form through Intl.PluralRules for the tag.
const plural = createPlural(code);
const { forms } = SELECTED[code]!;
// defineLocale fills in the direction from the tag, so Arabic reads right to left.
const { dir } = defineLocale({ code });
return (
<div className="languages-plurals">
<div role="group" aria-label="Language">
{Object.entries(SELECTED).map(([tag, { name }]) => (
<button key={tag} type="button" lang={tag} aria-pressed={tag === code} onClick={() => setCode(tag)}>
{name}
</button>
))}
</div>
<div role="group" aria-label="Count" className="languages-plurals__counts">
{COUNTS.map((value) => (
<button key={value} type="button" aria-pressed={value === count} onClick={() => setCount(value)}>
{value}
</button>
))}
</div>
<output lang={code} dir={dir} aria-live="polite">
{plural(count, forms)}
</output>
</div>
);
}createNumber(code) writes a number the way a tag does, and a malformed tag falls back to English instead of throwing. Every shipped pack is written with these two helpers, and a section can be a function of the active tag, as the Dutch table section above is, so one text serves every region of its language.
Calendars
The date picker is always Gregorian. Month and weekday names come from Intl in the active language, but the grid and every date written around it use the Gregorian calendar, even for tags such as ar-SA or fa whose default calendar is another. The value is written the same way in every language. Other calendars are on the roadmap.
Next.js
A pack holds functions, its plurals and number formatting, so it cannot be passed as a prop from a server component to a client component. Import packs in a "use client" file and let the server pass only the tag. In the App Router that file holds the provider:
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
import { SlotsmithProvider } from "slotsmith/provider";
import { ar } from "slotsmith/locales/ar";
export type Language = "en" | "ar";
const LanguageContext = createContext<{ language: Language; toggle: () => void }>({ language: "en", toggle: () => {} });
/** The client boundary: it holds the language and gives every slotsmith component below it the matching pack. */
export function Providers({ initialLanguage, children }: { initialLanguage: Language; children: ReactNode }) {
const [language, setLanguage] = useState(initialLanguage);
const toggle = () => {
const next = language === "en" ? "ar" : "en";
setLanguage(next);
document.cookie = `lang=${next}; path=/; max-age=31536000`;
document.documentElement.lang = next;
document.documentElement.dir = next === "ar" ? "rtl" : "ltr";
};
return (
<LanguageContext value={{ language, toggle }}>
{/* English is the default, so it needs no pack. */}
<SlotsmithProvider locale={language === "ar" ? "ar" : undefined} locales={[ar]}>
{children}
</SlotsmithProvider>
</LanguageContext>
);
}
/** A button that switches between English and Arabic. */
export function LanguageToggle() {
const { language, toggle } = useContext(LanguageContext);
return (
<button type="button" onClick={toggle}>
{language === "en" ? "العربية" : "English"}
</button>
);
}The root layout reads the language on the server, sets lang and dir on <html>, and passes the tag down:
import type { ReactNode } from "react";
import { cookies } from "next/headers";
import "slotsmith/data-table.css";
import { Providers } from "./providers";
export const metadata = { title: "slotsmith with Next.js" };
export default async function RootLayout({ children }: { children: ReactNode }) {
// The language lives in a cookie, so the server renders the right lang and dir from the first byte.
const language = (await cookies()).get("lang")?.value === "ar" ? "ar" : "en";
return (
<html lang={language} dir={language === "ar" ? "rtl" : "ltr"}>
<body>
<Providers initialLanguage={language}>{children}</Providers>
</body>
</html>
);
}Getting started has the whole example.
next-intl and i18next
If your app already translates with next-intl or i18next, keep your messages where they are. Build a locale with defineLocale whose labels call your t(), rebuild it when the language changes, and hand it to the provider. There is no adapter package to install.
With next-intl, write counts as ICU plurals in your messages and next-intl picks the form:
"use client";
// Display only: the docs do not install next-intl. Written against next-intl's public API.
import { useMemo, type ReactNode } from "react";
import { useLocale, useTranslations } from "next-intl";
import { defineLocale } from "slotsmith/locale";
import { SlotsmithProvider } from "slotsmith/provider";
// The labels live in your messages, under "slotsmith.datePicker".
export function SlotsmithIntl({ children }: { children: ReactNode }) {
const code = useLocale();
const t = useTranslations("slotsmith.datePicker");
// Write counts as ICU plurals in the messages, and next-intl picks the form:
// "count": "{count, plural, one {# date selected} other {# dates selected}}"
const locale = useMemo(
() =>
defineLocale({
code,
datePicker: {
placeholder: t("placeholder"),
clear: t("clear"),
previous: t("previous"),
next: t("next"),
today: t("today"),
month: t("month"),
year: t("year"),
dialog: t("dialog"),
count: (count) => t("count", { count }),
rangeStart: (from) => t("rangeStart", { from }),
range: (from, to) => t("range", { from, to }),
},
}),
[code, t],
);
return <SlotsmithProvider locale={locale}>{children}</SlotsmithProvider>;
}With i18next, plural forms come from your resources when t() is given count: one key per Intl.PluralRules category the language uses, suffixed with the category. English needs _one and _other; Arabic also needs _zero, _two, _few and _many:
"use client";
// Display only: the docs do not install i18next. Written against react-i18next's public API.
import { useMemo, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { defineLocale } from "slotsmith/locale";
import { SlotsmithProvider } from "slotsmith/provider";
// The labels live in your i18next resources, under a "slotsmith" namespace.
export function SlotsmithI18next({ children }: { children: ReactNode }) {
const { t, i18n } = useTranslation("slotsmith");
const code = i18n.resolvedLanguage ?? i18n.language;
// Rebuilt when the language changes. i18next picks plural forms itself, so a
// label that takes a count passes { count } to t().
const locale = useMemo(
() =>
defineLocale({
code,
datePicker: {
placeholder: t("datePicker.placeholder"),
clear: t("datePicker.clear"),
previous: t("datePicker.previous"),
next: t("datePicker.next"),
today: t("datePicker.today"),
month: t("datePicker.month"),
year: t("datePicker.year"),
dialog: t("datePicker.dialog"),
count: (count) => t("datePicker.count", { count }),
rangeStart: (from) => t("datePicker.rangeStart", { from }),
range: (from, to) => t("datePicker.range", { from, to }),
},
}),
[code, t],
);
return <SlotsmithProvider locale={locale}>{children}</SlotsmithProvider>;
}These two samples are shown, not run: the docs site does not install next-intl or i18next.