Localization & State
Translating every string; saving and restoring a view
Two features that tax every other one, so they are worth setting up early:
- Localization — every string the grid renders, overridable.
- Grid state — saved views: column layout, sort, filters, search, grouping, pagination, selection, scroll.
Neither needs a module.
Localization
Every string the grid renders has a key, and you override the ones you care about:
const grid = createGrid<Row>({
modules: [AllModules],
columns,
data,
locale: {
filterApply: 'Anwenden',
filterClear: 'Löschen',
filterOpContains: 'enthält',
},
})
Partial by design. Anything you leave out stays English, so overriding three strings for tone is as legitimate as translating all 250.
Parameterised strings are functions
Not strings with placeholder tokens — which is what lets a translation reorder or pluralise them:
locale: {
paginationPage: (page) => `Seite ${page}`,
paginationRowCount: (from, to, total) => `${from}–${to} von ${total}`,
groupCount: (count) => `${count} ${count === 1 ? 'Eintrag' : 'Einträge'}`,
}
A token-based string like 'Page {page} of {total}' cannot express a plural rule or a different word order. A function can.
Finding the keys
Keys are grouped by the surface they belong to, so autocomplete is useful:
| Prefix | Covers |
|---|---|
filterOp* |
Filter operators — contains, between, before… |
filter* |
The filter popover's own chrome |
floatingFilter* |
The floating filter row |
setFilter* |
The set filter's summary and list |
pagination* |
The pagination bar |
group* |
Group headings, counts, the chip bar, menu items |
The full list with its English defaults is DEFAULT_LOCALE in the package source.
Catching what you missed
const grid = createGrid<Row>({
modules: [AllModules],
columns,
data,
locale: myGermanLocale,
localeStrict: true, // dev-only
})
Warns once, listing every key still rendering English. Opt-in rather than on by default, because a partial override is a legitimate choice rather than a mistake.
The grid's own label
accessibility: {
gridLabel: 'Rechnungen', // overrides locale.gridLabel for this grid
}
accessibility.gridLabel predates locale and still wins for this one string — the narrow option beats the broad one.
Turning announcements down
accessibility: {
announceFocus: true, // default
announceSort: true, // default
announceFilter: true, // default
announceSelection: true, // default
announceEdit: true, // default
announcePage: true, // default
announceRowsLoaded: true, // default — "200 of 12,480 rows loaded"
}
All on by default. See Accessibility.
Grid state
Everything the user arranged, in one serialisable object:
const saved = api.getState()
localStorage.setItem('myGrid', JSON.stringify(saved))
const report = api.setState(JSON.parse(localStorage.getItem('myGrid')!))
Let the grid persist it for you
const grid = createGrid<Row>({
modules: [AllModules],
columns,
data,
state: {
key: 'invoices-grid', // persist here, restore on mount
debounce: 200, // default
},
})
That is the whole setup — no serialiser, no debounce, no version check to write.
debounce defaults to 200ms because scroll alone fires per frame, and an undebounced write would be a synchronous serialise on the scroll path.
What a state holds
| Slice | Contents |
|---|---|
columns |
Width, order, visibility, pinning — and which column groups are folded |
sort |
The sort model |
filters |
The filter model |
search |
The quick-filter text |
grouping |
{ model, expanded } — which columns are grouped and which sections are open |
pagination |
{ page, pageSize } |
selection |
{ ids } |
rowDetail |
{ expanded } — row ids whose detail panel is open |
scroll |
{ top, left } |
A slice is absent only when the module that owns it is not registered.
Group expansion survives a reload, because a group is addressed by its key path — plain strings that JSON handles with no custom serialiser, and nothing that points at an internal row object.
Choosing what to persist
state: {
key: 'invoices-grid',
include: ['columns', 'sort', 'filters'], // not selection or scroll
}
Default is every slice the grid produces.
Restoring a subset
api.setState(saved, { only: ['columns'] }) // layout back, filters untouched
A restore tells you what happened
const report = api.setState(saved)
if (report.skipped.length > 0) {
console.warn('partial restore', report.skipped)
}
| Field | Meaning |
|---|---|
applied |
Slices that were restored |
skipped |
{ slice, reason, detail? } for each one that was not |
reason |
Cause |
|---|---|
'absent-module' |
The module that owns the slice is not registered |
'not-in-state' |
The saved object has no such slice |
'excluded' |
You passed only and this slice was not in it |
'schema-mismatch' |
The saved state is from an incompatible version |
It returns a report rather than
void, which is what makes a partial restore visible — otherwise a saved view comes back "mostly" and nobody notices the filters are missing until a user does.expect(report.skipped).toEqual([])is a test anyone can write.
initial vs key
state: {
initial: savedFromMyServer, // restore this object once
}
initial is applied once, after the first data settle — not at construction. With an async dataSource there are no rows yet when the grid is built, so a restored selection would select nothing and a restored scroll offset would exceed a zero-height canvas.
Pipeline inputs — columns, sort, filters, search, page size — still apply immediately.
key and initial are independent: key persists to storage and restores from it, initial restores an object you supply. Use initial when the state comes from your own server rather than the browser.
Reacting to changes
events: {
onStateChange: (state) => saveToServer(state),
}
Already debounced by state.debounce, so it is safe to persist directly from here. It fires whether or not state.key is set.
Functions do not serialise
A comparator or customFilter declared on the column is re-attached on restore, so a column with a custom sort keeps it.
One passed imperatively — to setSortModel() or setColumnFilter() — is dropped, with a development warning. If you want a custom comparator to survive a saved view, declare it on the column.
Gotchas
localeis partial. Untranslated keys stay English silently;localeStrict: truein development is how you find them.- Parameterised locale entries are functions, not templates with tokens.
accessibility.gridLabelbeatslocale.gridLabel.state.initialapplies after the first data settle, not at construction — so a selection restore works with an async source.- Check
report.skipped. A half-restored view is otherwise invisible. - Declare comparators on the column, not imperatively, if they must survive a restore.
- A saved
rowDetailis filtered against the rows that came back — a saved view of a deleted row does not reopen a panel for it.
Next: 13 · Accessibility