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.
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.
| Group | Covers | Owning surface |
|---|---|---|
toolbar | Search placeholder and aria-labels, match counter, filter menu strings including the And/Or join operator and the per-row reorder announcement, columns menu | data-grid-toolbar |
sort | Sort button and aria-label, add and clear sort, ascending and descending, per-row remove and reorder arias, and the reorder announcement | data-grid-sort-list |
filterOperators | Per-operator display names (contains, equals, gt, and more) shown in the operator select of the filter menu | data-grid-toolbar |
contextMenu | Cut, copy, paste, row insert, duplicate, delete, sort, pin, hide, autosize, shared verbatim between the right-click menu and the header dropdown | data-grid-context-menu |
keybindings | Shortcuts dialog title and description, category headings, native-clipboard rows, and a per-GridAction label map | data-grid-keybindings |
markers | Select-all header checkbox and per-row checkbox aria-labels | core (rowMarkers) |
grid | Core strings not owned by an add-on: the empty-state message and the loading-skeleton and progress-bar aria-label | core |
io | Export dropdown, import dialog (file picker, delimiter, column mapping, preview, errors) | data-grid-io |
pagination | Footer first, prev, next, last, page-number aria-labels, page-size select, "x-y of z" range label | data-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.