gridcn

i18n

Translate every user-facing string via the typed labels object. No i18n library required.

Every user-facing string lives in one typed DataGridLabels object with English defaults.

Pick a language — direction follows it.
Name
Email
Role
Age
Silent Tiger
user0@example.com
Viewer
63
Quick Lion
user1@example.com
Viewer
67
Swift Wolf
user2@example.com
Manager
40
Eager Lion
user3@example.com
User
50
Eager Tiger
user4@example.com
Viewer
45
Silent Wolf
user5@example.com
User
24
Swift Lion
user6@example.com
Viewer
34
Eager Lion
user7@example.com
Manager
21
Eager Eagle
user8@example.com
Editor
25
Bold Tiger
user9@example.com
Editor
19

The labels prop

Pass a partial override to DataGridProvider (or DataGrid). It deep-merges over DEFAULT_LABELS, so only the keys you specify change, and everything else stays English:

<DataGridProvider
  labels={{
    toolbar: { searchPlaceholder: "Rechercher…" },
    contextMenu: { copy: "Copier", paste: "Coller" },
  }}
  {...grid}
  columns={columns}
/>

Interpolated strings are functions, not templates, so they receive real arguments, not placeholder tokens:

searchMatches: (current: number, total: number) => `${current}/${total}`,

Deep merging (DeepPartialLabels)

The merge is recursive per plain-object key. Arrays and functions replace the base value as a whole, rather than merging element by element. A function you provide fully replaces the default function, and gridcn does not merge the function itself:

export type DeepPartialLabels<T = DataGridLabels> = {
  [K in keyof T]?: T[K] extends (...args: never[]) => unknown
    ? T[K]
    : T[K] extends readonly unknown[]
      ? T[K]
      : T[K] extends object
        ? DeepPartialLabels<T[K]>
        : T[K];
};

In practice, overriding toolbar.searchPlaceholder alone leaves every other toolbar.* string, and every other group, at its English default. You never have to restate the whole shape to change one word.

Reading effective labels

Call useDataGridLabels() inside any component under DataGridProvider to read the merged labels, defaults plus your overrides, for your own custom UI to use. This is useful when you build a bespoke toolbar button or empty-state message that stays consistent with the rest of the grid's copy.

Coverage by group

DataGridLabels groups strings by owning surface. Every group is optional to override. Omit a group entirely, and it stays fully English.

GroupCoversOwning surface
toolbarSearch placeholder and aria-labels, match counter, filter menu strings including the And/Or join operator and the per-row reorder announcement, columns menudata-grid-toolbar
sortSort button and aria-label, add and clear sort, ascending and descending, per-row remove and reorder arias, and the reorder announcementdata-grid-sort-list
filterOperatorsPer-operator display names (contains, equals, gt, and more) shown in the operator select of the filter menudata-grid-toolbar
contextMenuCut, copy, paste, row insert, duplicate, delete, sort, pin, hide, autosize, shared verbatim between the right-click menu and the header dropdowndata-grid-context-menu
keybindingsShortcuts dialog title and description, category headings, native-clipboard rows, and a per-GridAction label mapdata-grid-keybindings
markersSelect-all header checkbox and per-row checkbox aria-labelscore (rowMarkers)
gridCore strings not owned by an add-on: the empty-state message and the loading-skeleton and progress-bar aria-labelcore
ioExport dropdown, import dialog (file picker, delimiter, column mapping, preview, errors)data-grid-io
paginationFooter first, prev, next, last, page-number aria-labels, page-size select, "x-y of z" range labeldata-grid-pagination

Full shape lives in API reference

The exact type of every field of every group is generated from source in API reference. This page covers the merge model and gives a worked translation. That page is the field-by-field reference.

Full German example

A complete override translates every group. Omit any group or key that you do not need to change. This example is deliberately exhaustive, to show the full shape in one place:

import type { DeepPartialLabels } from "@/components/data-grid/data-grid";

const de: DeepPartialLabels = {
  toolbar: {
    searchPlaceholder: "Suchen…",
    searchAriaLabel: "Tabelle durchsuchen",
    searchPreviousMatch: "Vorheriger Treffer",
    searchNextMatch: "Nächster Treffer",
    searchMatches: (current, total) => `${current}/${total}`,
    searchMatchesCapped: (current) => `${current}/1000+`,
    searchNoMatches: "0/0",
    filter: "Filter",
    filterAriaLabel: "Filter",
    filterColumnAriaLabel: "Spalte filtern",
    filterOperatorAriaLabel: "Filteroperator",
    filterValuePlaceholder: "Wert",
    filterValueAriaLabel: "Filterwert",
    filterValueFromAriaLabel: "Filterwert von",
    filterValueToAriaLabel: "Filterwert bis",
    filterValueTrue: "wahr",
    filterValueFalse: "falsch",
    removeFilterAriaLabel: "Filter entfernen",
    reorderFilterAriaLabel: "Filter verschieben",
    filterReorderAnnouncement: (column, position, total) => `${column}-Filter an Position ${position} von ${total} verschoben`,
    addFilter: "Filter hinzufügen",
    clearFilters: "Alle löschen",
    noFiltersApplied: "Keine Filter angewendet.",
    filterWhere: "Wobei",
    joinOperatorAriaLabel: "Verknüpfung",
    joinOperatorAnd: "Und",
    joinOperatorOr: "Oder",
    columns: "Spalten",
    columnsAriaLabel: "Spalten",
  },
  sort: {
    sort: "Sortieren",
    sortAriaLabel: "Sortierungen",
    columnAriaLabel: "Sortierspalte",
    directionAriaLabel: "Sortierrichtung",
    ascending: "Aufsteigend",
    descending: "Absteigend",
    removeSortAriaLabel: "Sortierung entfernen",
    reorderSortAriaLabel: "Sortierung verschieben",
    sortReorderAnnouncement: (column, position, total) => `${column}-Sortierung an Position ${position} von ${total} verschoben`,
    addSort: "Sortierung hinzufügen",
    clearSorts: "Alle löschen",
    noSortsApplied: "Keine Sortierung angewendet.",
  },
  filterOperators: {
    contains: "enthält",
    notContains: "enthält nicht",
    equals: "ist gleich",
    notEquals: "ist ungleich",
    startsWith: "beginnt mit",
    endsWith: "endet mit",
    empty: "ist leer",
    notEmpty: "ist nicht leer",
    gt: "größer als",
    gte: "größer oder gleich",
    lt: "kleiner als",
    lte: "kleiner oder gleich",
    isBetween: "liegt zwischen",
  },
  contextMenu: {
    cut: "Ausschneiden",
    copy: "Kopieren",
    paste: "Einfügen",
    pasteBlocked: "erfordert Zwischenablage-Berechtigung — Strg+V verwenden",
    clearContents: "Inhalt löschen",
    insertRowAbove: "Zeile oberhalb einfügen",
    insertRowBelow: "Zeile unterhalb einfügen",
    duplicateRow: "Zeile duplizieren",
    duplicateRows: (count) => (count > 1 ? "Zeilen duplizieren" : "Zeile duplizieren"),
    deleteRow: "Zeile löschen",
    deleteRows: (count) => (count > 1 ? "Zeilen löschen" : "Zeile löschen"),
    sortAsc: "Aufsteigend sortieren",
    sortDesc: "Absteigend sortieren",
    clearSort: "Sortierung entfernen",
    pinLeft: "Links anheften",
    pinRight: "Rechts anheften",
    unpin: "Lösen",
    hideColumn: "Spalte ausblenden",
    autosize: "Spaltenbreite anpassen",
    columnMenuAriaLabel: (column) => `${column} Spaltenmenü`,
  },
  keybindings: {
    title: "Tastaturkürzel",
    description: "Alle in dieser Tabelle aktiven Tastenkombinationen.",
    categories: {
      navigation: "Navigation",
      selection: "Auswahl",
      editing: "Bearbeitung",
      clipboardFill: "Zwischenablage & Ausfüllen",
      history: "Verlauf",
      other: "Sonstiges",
    },
    nativeCopy: "Kopieren",
    nativeCut: "Ausschneiden",
    nativePaste: "Einfügen",
    actions: {
      undo: "Rückgängig",
      redo: "Wiederholen",
      selectAll: "Alles auswählen",
      // ...remaining GridAction keys fall back to an auto-humanized name when omitted
    },
  },
  markers: {
    selectAll: "Alle Zeilen auswählen",
    selectRow: (rowNumber) => `Zeile ${rowNumber} auswählen`,
  },
  grid: {
    emptyState: "Keine Zeilen",
    loading: "Wird geladen…",
  },
  io: {
    exportButtonAriaLabel: "Exportieren",
    exportXlsx: "Als Excel exportieren (.xlsx)",
    exportCsv: "Als CSV exportieren",
    importButton: "Importieren",
    importDialogTitle: "Datei importieren",
    importDialogDescription: "CSV- oder Excel-Datei wählen und Spalten der Tabelle zuordnen.",
    chooseFile: "Datei wählen",
    noFileChosen: "Keine Datei ausgewählt",
    columnFallback: (index) => `Spalte ${index}`,
    delimiter: "Trennzeichen",
    delimiterComma: "Komma (,)",
    delimiterSemicolon: "Semikolon (;)",
    delimiterTab: "Tabulator",
    hasHeaderRow: "Erste Zeile ist Kopfzeile",
    mapColumns: "Spalten zuordnen",
    mapColumnAriaLabel: (importColumn) => `„${importColumn}" einer Spalte zuordnen`,
    skipColumn: "— Überspringen —",
    skipColumnQuick: "Spalte überspringen",
    preview: "Vorschau",
    previewTruncated: (shown, total) => `${shown} von ${total} Zeilen angezeigt`,
    sheet: "Blatt",
    import: "Importieren",
    cancel: "Abbrechen",
    errorParseFailed: "Datei konnte nicht gelesen werden.",
    errorNoRows: "Keine Zeilen in dieser Datei gefunden.",
    errorUnsupportedFile: "Nicht unterstützter Dateityp — CSV, .xlsx oder .xls wählen.",
  },
};

<DataGridProvider labels={de} {...grid} columns={columns}>

keybindings.actions is a partial map

keybindings.actions is Partial<Record<GridAction, string>>. Any GridAction that you do not list falls back to an auto-humanized version of its name (for example moveUp becomes "Move up"). This is not English text that silently leaks through untranslated forever. Add entries as you notice gaps.

Wiring a real i18n library

gridcn has no i18n library dependency. labels is a plain object, so it composes with whatever i18n library your app already uses — react-i18next, next-intl, or anything else. Build it from your translation function instead of hardcoding strings:

import { useTranslation } from "react-i18next";

function OrdersGrid() {
  const { t } = useTranslation("grid");
  const grid = useDataGridState(initialRows, { getRowId: (row) => row.id });

  const labels: DeepPartialLabels = {
    toolbar: { searchPlaceholder: t("toolbar.searchPlaceholder") },
    grid: { emptyState: t("grid.emptyState") },
  };

  return (
    <DataGridProvider labels={labels} {...grid} columns={columns}>
      <DataGridRoot>
        <DataGridHeader />
        <DataGridBody />
      </DataGridRoot>
    </DataGridProvider>
  );
}

Pass a stable labels reference

Like columns, data, and getRowClassName, labels becomes a prop that the engine depends on. Build it with useMemo, keyed on your active locale, rather than a fresh object literal on every render.

RTL layout

labels translates strings. It does not flip layout. direction flips layout. It does not translate strings. Most apps derive both from one active locale, as the demo above does:

const TRANSLATIONS = {
  en: { dir: "ltr", values: { searchPlaceholder: "Search…" } },
  ar: { dir: "rtl", values: { searchPlaceholder: "بحث…" } },
} as const;

const { dir, values } = TRANSLATIONS[locale];

<DataGridProvider labels={{ toolbar: values }} {...grid} columns={columns}>
  <DataGridRoot direction={dir}>
    <DataGridHeader />
    <DataGridBody />
  </DataGridRoot>
</DataGridProvider>

Omit direction and the grid reads the direction it inherits from the page, so a grid inside <html dir="rtl"> is right-to-left with no prop. Pass direction to force one direction independent of the page.

What mirrors

Column order, pinned column bands, the row-marker column, pin shadows, selection rectangles, resize handles, and drop indicators all mirror. Pointer hit-testing, drag auto-scroll, column resize, and column reorder are correct in both directions.

Arrow keys move visually: under rtl, ArrowRight moves to the next column on the screen, which is the previous column index. This matches spreadsheet behavior on right-to-left systems.

Tab and Shift+Tab keep their reading order and do not flip. Home and End stay logical: Home goes to the first column in both directions.

Cell values render inside <bdi>, so an Arabic value in a left-to-right grid still reads right-to-left inside its own cell, and a Latin value in a right-to-left grid reads left-to-right. Alignment stays the grid's: a start-aligned column hugs the inline start whatever script its values are in. Cell editors carry dir="auto" instead, so the caret follows the value being typed.

Base UI chrome

DataGridRoot sets dir on the grid element and wraps its subtree in Base UI's DirectionProvider. Both are necessary: dir drives CSS logical properties and rtl: variants, DirectionProvider drives Base UI's own positioning and keyboard navigation. Menus, popovers, and dialogs from the add-ons need no extra setup.

Portaled content that you render yourself, outside the grid subtree, needs dir passed to it explicitly.

Set `dir` on the document too

The grid element carries its own dir, but page chrome around it does not. Set dir="rtl" on <html> or a layout wrapper so the rest of your interface mirrors with the grid.

On this page