# Languages

> The 18 locale packs slotsmith ships, how to pass one to a component or a whole app, write your own, override single labels, and bridge next-intl or i18next.

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](https://github.com/tmzm/slotsmith/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.

```tsx
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>
  );
}
```

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:

```tsx
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](https://slotsmith.dev/ar/guides/#once-for-the-whole-app).

```tsx
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.

```tsx
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:

```tsx
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](https://slotsmith.dev/ar/components/data-table/api/#labels).

## Precedence

Every string is resolved on its own. Each step overrides the one before it:

1. The English default.
2. The `locale` of the nearest `SlotsmithProvider`.
3. The component's own `locale`. It replaces the provider's whole, so one component never mixes two languages.
4. The component's `labels` prop.

```tsx
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:

```tsx
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](https://slotsmith.dev/ar/roadmap/#calendars).

## 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:

```tsx
"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:

```tsx
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](https://slotsmith.dev/ar/getting-started/#nextjs-and-vite) 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:

```tsx
"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`:

```tsx
"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.
