@opencorestack/opengridx 3.8.1 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +340 -1
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +7529 -7285
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +93 -4
  9. package/docs/customization/theming.md +11 -2
  10. package/docs/features/ai-toolkit.md +1 -1
  11. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
  12. package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
  13. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
  14. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
  15. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
  16. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
  17. package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
  18. package/lib/components/DataGrid/GridListView.tsx +7 -5
  19. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
  20. package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
  21. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  22. package/lib/components/DataGrid/GridRoot.tsx +8 -1
  23. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  24. package/lib/components/DataGrid/GridViewport.tsx +3 -1
  25. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  26. package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
  27. package/lib/components/FilterPanel/FilterPanel.tsx +18 -17
  28. package/lib/components/Header/ColumnMenu.css +5 -5
  29. package/lib/components/Header/ColumnMenu.tsx +17 -15
  30. package/lib/components/Header/Header.tsx +8 -6
  31. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
  32. package/lib/components/ListView/ListViewRow.tsx +3 -1
  33. package/lib/components/Pagination/Pagination.tsx +15 -18
  34. package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
  35. package/lib/components/Row/Row.tsx +5 -3
  36. package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
  37. package/lib/components/Toolbar/GridToolbar.tsx +35 -29
  38. package/lib/components/Toolbar/PivotPanel.tsx +26 -24
  39. package/lib/components/ui/Checkbox.css +5 -5
  40. package/lib/components/ui/DragHandleIcon.tsx +3 -1
  41. package/lib/components/ui/ExpandIcon.css +3 -3
  42. package/lib/components/ui/ExpandIcon.tsx +3 -1
  43. package/lib/hooks/core/useGridRowInteractions.ts +13 -3
  44. package/lib/hooks/core/useGridToolbarStack.ts +2 -2
  45. package/lib/hooks/core/useGridVirtualization.ts +40 -25
  46. package/lib/hooks/core/useShallowStableValue.ts +29 -0
  47. package/lib/hooks/features/useGridAiAssistant.ts +52 -22
  48. package/lib/theme/DataGridThemeProvider.tsx +9 -0
  49. package/lib/theme/types.ts +18 -0
  50. package/lib/types/locale.ts +344 -0
  51. package/lib/utils/gridLocale.ts +241 -0
  52. package/llms.txt +3 -1
  53. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibility
4
4
  import type { GridColDef, GridRowModel, GridColumnGroupingModel } from '../../types';
5
5
  import { canReorderWithinColumnGroups } from '../../utils/columnGroups';
6
6
  import { getViewportWidth } from '../../utils/viewport';
7
+ import { useGridLocale } from './gridLocaleContext';
7
8
 
8
9
  export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
9
10
  isOpen: boolean;
@@ -42,6 +43,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
42
43
  onColumnOrderReset,
43
44
  columnGroupingModel,
44
45
  }: GridStandaloneColumnPanelProps<R>) {
46
+ const locale = useGridLocale();
45
47
  // Resolved from the DOM after mount, never during render, so the grid renders on the server.
46
48
  const [placement, setPlacement] = useState<PanelPlacement | null>(null);
47
49
 
@@ -97,7 +99,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
97
99
  zIndex: 1,
98
100
  boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
99
101
  }}
100
- aria-label="Close"
102
+ aria-label={locale.columnsPanelClose}
101
103
  >×</button>
102
104
  <ColumnVisibilityPanel<R>
103
105
  columns={columns}
@@ -4,6 +4,7 @@ import { GridPinnedRows, type GridPinnedRowsProps } from './GridPinnedRows';
4
4
  import { GridVirtualRows, type GridVirtualRowsProps } from './GridVirtualRows';
5
5
  import { GridEmptyState } from './GridEmptyState';
6
6
  import { GridAggregationFooter } from './GridAggregationFooter';
7
+ import { useGridLocale } from './gridLocaleContext';
7
8
  import type { GridAggregationModel, GridAggregationResult, GridColDef, GridColumnPinning, GridRowId, GridRowModel } from '../../types';
8
9
  import type { GridAriaRowLayout } from '../../utils/aria';
9
10
  import type { UseGridCellSelectionReturn } from '../../hooks/core/useGridCellSelection';
@@ -120,6 +121,7 @@ export interface GridViewportProps<R extends GridRowModel> {
120
121
  * empty state, the virtual centre rows and the aggregation footer.
121
122
  */
122
123
  export function GridViewport<R extends GridRowModel>(props: GridViewportProps<R>) {
124
+ const locale = useGridLocale();
123
125
  const {
124
126
  setViewportElement, handleScroll, ariaLabel, ariaRows, navigationColumnCount, rowSelectionEnabled,
125
127
  disableMultipleRowSelection, cellSelection, effectiveLoading, viewportTabIndex, setKeyboardMode,
@@ -165,7 +167,7 @@ export function GridViewport<R extends GridRowModel>(props: GridViewportProps<R>
165
167
  className="ogx__viewport"
166
168
  onScroll={handleScroll}
167
169
  role="grid"
168
- aria-label={ariaLabel || 'Data grid'}
170
+ aria-label={ariaLabel || locale.gridLabel}
169
171
  aria-rowcount={ariaRows.rowCount}
170
172
  aria-colcount={navigationColumnCount}
171
173
  aria-multiselectable={(rowSelectionEnabled && !disableMultipleRowSelection) || cellSelection.enabled}
@@ -208,7 +208,7 @@ export function GridVirtualRows<R extends GridRowModel>({
208
208
  onDrop={rowReorderHandlers.onDrop}
209
209
  isDragging={rowReorderHandlers.draggedRowId === id}
210
210
  isDragOver={rowReorderHandlers.dragOverRowId === id}
211
- editingCell={editingHandlers.editingCell}
211
+ editingCell={editingHandlers.editingCell?.id === id ? editingHandlers.editingCell : null}
212
212
  onEditStart={editingHandlers.startCellEdit}
213
213
  onEditStop={editingHandlers.stopCellEdit}
214
214
  onEditCellValueChange={editingHandlers.setEditCellValue}
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { resolveGridLocale, type ResolvedGridLocale } from '../../utils/gridLocale';
3
+
4
+ /**
5
+ * The grid's resolved `localeText`, provided once by `GridRoot`. Components rendered outside a grid
6
+ * (the exported `ColumnVisibilityPanel`, `GridToolbar`, `FilterPanel`, …) get the English default.
7
+ */
8
+ export const GridLocaleContext = createContext<ResolvedGridLocale>(resolveGridLocale(undefined));
9
+
10
+ /** The strings of the enclosing grid, English outside one. */
11
+ export function useGridLocale(): ResolvedGridLocale {
12
+ return useContext(GridLocaleContext);
13
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
10
  import { filterValueFromInputText, getFilterValueOptions, isSameFilterValue, matchesFilterValueOption } from '../../utils/filtering/filterInput';
11
11
  import { Input } from '../ui/Input';
12
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
12
13
 
13
14
  export interface FilterPanelProps {
14
15
  filterModel: GridFilterModel;
@@ -69,6 +70,7 @@ const FilterRow: React.FC<{
69
70
  onChange: (item: GridFilterItem | null) => void;
70
71
  }> = ({ col, item, resetToken, onChange }) => {
71
72
  const filterId = useId();
73
+ const locale = useGridLocale();
72
74
  // Memoized so React Compiler treats the list as frozen and can keep the callbacks below memoized.
73
75
  const typeOperators = useMemo(() => getOperatorsForType(col.type), [col.type]);
74
76
  const defaultOperator = typeOperators[0];
@@ -153,7 +155,7 @@ const FilterRow: React.FC<{
153
155
  const showValue = !NO_VALUE_OPERATORS.has(currentOperator);
154
156
  const valueOptions = getFilterValueOptions(col);
155
157
  const valueClassName = `ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`;
156
- const valueLabel = `Filter value for ${label}`;
158
+ const valueLabel = locale.filterPanelValueLabel(label);
157
159
 
158
160
  let valueControl: React.ReactNode = null;
159
161
  if (showValue && col.type === 'boolean') {
@@ -169,9 +171,9 @@ const FilterRow: React.FC<{
169
171
  }}
170
172
  aria-label={valueLabel}
171
173
  >
172
- <option value="">Any</option>
173
- <option value="true">true</option>
174
- <option value="false">false</option>
174
+ <option value="">{locale.filterPanelAny}</option>
175
+ <option value="true">{locale.filterPanelTrue}</option>
176
+ <option value="false">{locale.filterPanelFalse}</option>
175
177
  </select>
176
178
  );
177
179
  } else if (showValue && col.type === 'singleSelect' && valueOptions.length > 0 && currentOperator === 'isAnyOf') {
@@ -207,7 +209,7 @@ const FilterRow: React.FC<{
207
209
  }}
208
210
  aria-label={valueLabel}
209
211
  >
210
- <option value="">Any</option>
212
+ <option value="">{locale.filterPanelAny}</option>
211
213
  {valueOptions.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
212
214
  </select>
213
215
  );
@@ -220,7 +222,7 @@ const FilterRow: React.FC<{
220
222
  type={col.type === 'date' ? 'date' : 'text'}
221
223
  value={draft ?? toInputText(item?.value, col)}
222
224
  onChange={(e) => setDraft(e.target.value)}
223
- placeholder={col.type === 'number' ? 'Number…' : 'Value…'}
225
+ placeholder={col.type === 'number' ? locale.filterPanelNumberPlaceholder : locale.filterPanelValuePlaceholder}
224
226
  aria-label={valueLabel}
225
227
  />
226
228
  );
@@ -239,10 +241,10 @@ const FilterRow: React.FC<{
239
241
  className={`ogx-filter__op-select${isActive ? ' ogx-filter__op-select--active' : ''}`}
240
242
  value={currentOperator}
241
243
  onChange={(e) => handleOperatorChange(e.target.value as GridFilterOperator)}
242
- aria-label={`Filter operator for ${label}`}
244
+ aria-label={locale.filterPanelOperatorLabel(label)}
243
245
  >
244
246
  {operators.map(op => (
245
- <option key={op} value={op}>{op}</option>
247
+ <option key={op} value={op}>{locale.filterPanelOperator(op)}</option>
246
248
  ))}
247
249
  </select>
248
250
 
@@ -254,8 +256,8 @@ const FilterRow: React.FC<{
254
256
  type="button"
255
257
  className="ogx-filter__row-clear"
256
258
  onClick={handleClear}
257
- aria-label={`Clear filter for ${label}`}
258
- title="Clear"
259
+ aria-label={locale.filterPanelClearLabel(label)}
260
+ title={locale.filterPanelClearTooltip}
259
261
  >
260
262
  ×
261
263
  </button>
@@ -268,6 +270,7 @@ const FilterRow: React.FC<{
268
270
  // ── Root panel ────────────────────────────────────────────────────────────────
269
271
 
270
272
  export const FilterPanel: React.FC<FilterPanelProps> = ({ filterModel, columns, onFilterModelChange }) => {
273
+ const locale = useGridLocale();
271
274
  const items = useMemo<FilterEntry[]>(() => filterModel.items ?? [], [filterModel.items]);
272
275
  const logicOperator = filterModel.logicOperator ?? 'and';
273
276
 
@@ -313,14 +316,14 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filterModel, columns,
313
316
  <>
314
317
  {items.length > 1 && (
315
318
  <div className="ogx-filter__logic-bar">
316
- <span className="ogx-filter__logic-label">Match:</span>
319
+ <span className="ogx-filter__logic-label">{locale.filterPanelMatch}</span>
317
320
  <button
318
321
  type="button"
319
322
  className={`ogx-filter__logic-pill${logicOperator === 'and' ? ' ogx-filter__logic-pill--active' : ''}`}
320
323
  onClick={() => handleLogicChange('and')}
321
324
  aria-pressed={logicOperator === 'and'}
322
325
  >
323
- ALL (AND)
326
+ {locale.filterPanelAnd}
324
327
  </button>
325
328
  <button
326
329
  type="button"
@@ -328,21 +331,19 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filterModel, columns,
328
331
  onClick={() => handleLogicChange('or')}
329
332
  aria-pressed={logicOperator === 'or'}
330
333
  >
331
- ANY (OR)
334
+ {locale.filterPanelOr}
332
335
  </button>
333
336
  </div>
334
337
  )}
335
338
 
336
339
  {notShownCount > 0 && (
337
340
  <div className="ogx-filter__hidden-note" role="note">
338
- {notShownCount === 1
339
- ? '1 more condition (a filter group or a second condition on a column) is applied and not shown here.'
340
- : `${notShownCount} more conditions (filter groups or further conditions on a column) are applied and not shown here.`}
341
+ {locale.filterPanelHiddenConditions(notShownCount)}
341
342
  </div>
342
343
  )}
343
344
 
344
345
  {filterableColumns.length === 0 ? (
345
- <div className="ogx-filter__empty">No filterable columns.</div>
346
+ <div className="ogx-filter__empty">{locale.filterPanelNoColumns}</div>
346
347
  ) : (
347
348
  filterableColumns.map(col => (
348
349
  <FilterRow
@@ -14,7 +14,7 @@
14
14
  font-weight: 600;
15
15
  text-transform: uppercase;
16
16
  letter-spacing: 0.06em;
17
- color: var(--ogx-color-gray-400, #94a3b8);
17
+ color: var(--ogx-overlay-section-label-text, var(--ogx-color-gray-400, #94a3b8));
18
18
  user-select: none;
19
19
  }
20
20
  .ogx-menu-item {
@@ -49,21 +49,21 @@
49
49
  display: flex;
50
50
  align-items: center;
51
51
  justify-content: center;
52
- color: var(--ogx-overlay-item-selected-text, #1976d2);
52
+ color: var(--ogx-overlay-item-indicator, var(--ogx-overlay-item-selected-text, #1976d2));
53
53
  }
54
54
  .ogx-menu-item__icon {
55
55
  flex: 0 0 16px;
56
56
  display: flex;
57
57
  align-items: center;
58
58
  justify-content: center;
59
- color: var(--ogx-color-gray-400, #94a3b8);
59
+ color: var(--ogx-overlay-icon, var(--ogx-color-gray-400, #94a3b8));
60
60
  transition: color 0.1s;
61
61
  }
62
62
  .ogx-menu-item--active .ogx-menu-item__icon {
63
- color: var(--ogx-color-primary, #1976d2);
63
+ color: var(--ogx-overlay-item-selected-icon, var(--ogx-color-primary, #1976d2));
64
64
  }
65
65
  .ogx-menu-divider {
66
66
  height: 1px;
67
- background: var(--ogx-overlay-border, #e2e8f0);
67
+ background: var(--ogx-overlay-divider, var(--ogx-overlay-border, #e2e8f0));
68
68
  margin: 4px 0;
69
69
  }
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
3
3
  import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning } from '../../types';
4
4
  import { getViewportWidth, getViewportHeight } from '../../utils/viewport';
5
5
  import { isRtl } from '../../utils/direction';
6
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
6
7
 
7
8
 
8
9
  interface ColumnMenuProps {
@@ -123,6 +124,7 @@ const menuItemsOf = (menu: HTMLElement | null): HTMLElement[] =>
123
124
 
124
125
  export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedColumns, onManageColumns, onClose, anchorEl }: ColumnMenuProps) {
125
126
  const menuRef = useRef<HTMLDivElement>(null);
127
+ const locale = useGridLocale();
126
128
 
127
129
  // Closes the menu; when it was left from the keyboard or by choosing an item, focus goes back
128
130
  // to the header cell that owns it (WAI-ARIA menu button pattern).
@@ -234,7 +236,7 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
234
236
  const colLabel = colDef.headerName || colDef.field;
235
237
 
236
238
  return ReactDOM.createPortal(
237
- <div ref={menuRef} className="ogx-column-menu" style={style} role="menu" aria-label={`Column options for ${colLabel}`} onKeyDown={handleMenuKeyDown}>
239
+ <div ref={menuRef} className="ogx-column-menu" style={style} role="menu" aria-label={locale.columnMenuLabel(colLabel)} onKeyDown={handleMenuKeyDown}>
238
240
 
239
241
  { }
240
242
  {onSort && colDef.sortable !== false && (
@@ -243,19 +245,19 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
243
245
  onClick={() => handleSort('asc')} role="menuitemradio" aria-checked={currentSort === 'asc'}>
244
246
  <span className="ogx-menu-item__indicator" aria-hidden="true">{currentSort === 'asc' ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
245
247
  <span className="ogx-menu-item__icon" aria-hidden="true"><AscIcon /></span>
246
- Sort Ascending
248
+ {locale.columnMenuSortAscending}
247
249
  </button>
248
250
  <button type="button" className={`ogx-menu-item${currentSort === 'desc' ? ' ogx-menu-item--active' : ''}`}
249
251
  onClick={() => handleSort('desc')} role="menuitemradio" aria-checked={currentSort === 'desc'}>
250
252
  <span className="ogx-menu-item__indicator" aria-hidden="true">{currentSort === 'desc' ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
251
253
  <span className="ogx-menu-item__icon" aria-hidden="true"><DescIcon /></span>
252
- Sort Descending
254
+ {locale.columnMenuSortDescending}
253
255
  </button>
254
256
  <button type="button" className={`ogx-menu-item${isUnsorted ? ' ogx-menu-item--active' : ''}`}
255
257
  onClick={() => handleSort(null)} role="menuitemradio" aria-checked={isUnsorted}>
256
258
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isUnsorted ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
257
259
  <span className="ogx-menu-item__icon" aria-hidden="true"><UnsortIcon /></span>
258
- Unsort
260
+ {locale.columnMenuUnsort}
259
261
  </button>
260
262
  <div className="ogx-menu-divider" />
261
263
  </>
@@ -264,24 +266,24 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
264
266
  { }
265
267
  {canPin && (
266
268
  <>
267
- <div className="ogx-menu-section-label" aria-hidden="true">Pin column</div>
269
+ <div className="ogx-menu-section-label" aria-hidden="true">{locale.columnMenuPinColumn}</div>
268
270
  <button type="button" className={`ogx-menu-item${isPinnedScreenLeft ? ' ogx-menu-item--active' : ''}`}
269
- onClick={() => handlePin(screenLeftSide)} role="menuitemradio" aria-checked={isPinnedScreenLeft} aria-label="Pin to left">
271
+ onClick={() => handlePin(screenLeftSide)} role="menuitemradio" aria-checked={isPinnedScreenLeft} aria-label={locale.columnMenuPinLeftLabel}>
270
272
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedScreenLeft ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
271
273
  <span className="ogx-menu-item__icon" aria-hidden="true"><PinLeftIcon /></span>
272
- Pin to Left
274
+ {locale.columnMenuPinLeft}
273
275
  </button>
274
276
  <button type="button" className={`ogx-menu-item${isPinnedScreenRight ? ' ogx-menu-item--active' : ''}`}
275
- onClick={() => handlePin(screenRightSide)} role="menuitemradio" aria-checked={isPinnedScreenRight} aria-label="Pin to right">
277
+ onClick={() => handlePin(screenRightSide)} role="menuitemradio" aria-checked={isPinnedScreenRight} aria-label={locale.columnMenuPinRightLabel}>
276
278
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedScreenRight ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
277
279
  <span className="ogx-menu-item__icon" aria-hidden="true"><PinRightIcon /></span>
278
- Pin to Right
280
+ {locale.columnMenuPinRight}
279
281
  </button>
280
282
  <button type="button" className={`ogx-menu-item${isUnpinned ? ' ogx-menu-item--active' : ''}`}
281
- onClick={() => handlePin(null)} role="menuitemradio" aria-checked={isUnpinned} aria-label="Unpin column">
283
+ onClick={() => handlePin(null)} role="menuitemradio" aria-checked={isUnpinned} aria-label={locale.columnMenuUnpinLabel}>
282
284
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isUnpinned ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
283
285
  <span className="ogx-menu-item__icon" aria-hidden="true"><UnpinIcon /></span>
284
- Unpin
286
+ {locale.columnMenuUnpin}
285
287
  </button>
286
288
  <div className="ogx-menu-divider" />
287
289
  </>
@@ -289,19 +291,19 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
289
291
 
290
292
  {canHide && (
291
293
  <>
292
- <button type="button" className="ogx-menu-item" onClick={handleHide} role="menuitem" aria-label={`Hide ${colLabel} column`}>
294
+ <button type="button" className="ogx-menu-item" onClick={handleHide} role="menuitem" aria-label={locale.columnMenuHideColumnLabel(colLabel)}>
293
295
  <span className="ogx-menu-item__indicator" aria-hidden="true" />
294
296
  <span className="ogx-menu-item__icon" aria-hidden="true"><HideIcon /></span>
295
- Hide Column
297
+ {locale.columnMenuHideColumn}
296
298
  </button>
297
299
  <div className="ogx-menu-divider" />
298
300
  </>
299
301
  )}
300
302
 
301
- <button type="button" className="ogx-menu-item" onClick={() => { onManageColumns?.(); close(false); }} role="menuitem" aria-label="Manage all columns">
303
+ <button type="button" className="ogx-menu-item" onClick={() => { onManageColumns?.(); close(false); }} role="menuitem" aria-label={locale.columnMenuManageColumnsLabel}>
302
304
  <span className="ogx-menu-item__indicator" aria-hidden="true" />
303
305
  <span className="ogx-menu-item__icon" aria-hidden="true"><ManageColumnsIcon /></span>
304
- Manage columns
306
+ {locale.columnMenuManageColumns}
305
307
  </button>
306
308
  </div>,
307
309
  (anchorEl?.closest('.ogx-theme-provider') || document.body)
@@ -11,6 +11,7 @@ import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap
11
11
  import { getHeaderColumnStyle } from './headerColumnStyle';
12
12
  import { HeaderFilterRow } from '../HeaderFilter/HeaderFilterRow';
13
13
  import { isRtl, logicalArrowKey } from '../../utils/direction';
14
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
14
15
 
15
16
 
16
17
  function MenuIcon() {
@@ -93,6 +94,7 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
93
94
  }
94
95
 
95
96
  export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
97
+ const locale = useGridLocale();
96
98
  const {
97
99
  columns,
98
100
  allColumns,
@@ -348,11 +350,11 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
348
350
  <div
349
351
  className={`ogx__header-cell ogx__header-cell--pinned ogx__header-cell--pinned-left ${isHeaderFocused('__reorder_col__') ? 'ogx__header-cell--focused' : ''}`}
350
352
  role="columnheader"
351
- aria-label="Row reorder handle"
353
+ aria-label={locale.headerRowReorderLabel}
352
354
  aria-colindex={reorderColIndex}
353
355
  data-field="__reorder_col__"
354
356
  tabIndex={-1}
355
- title="Drag to reorder rows"
357
+ title={locale.headerRowReorderTooltip}
356
358
  style={{
357
359
  minWidth: 48,
358
360
  maxWidth: 48,
@@ -368,7 +370,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
368
370
  className={`ogx__header-cell ogx__header-cell--expand ${pinExpandColumn ? 'ogx__header-cell--pinned ogx__header-cell--pinned-left' : ''
369
371
  } ${isHeaderFocused('__expand_col__') ? 'ogx__header-cell--focused' : ''}`}
370
372
  role="columnheader"
371
- aria-label="Expand row details"
373
+ aria-label={locale.headerDetailPanelLabel}
372
374
  aria-colindex={expandColIndex}
373
375
  data-field="__expand_col__"
374
376
  tabIndex={-1}
@@ -405,7 +407,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
405
407
  checked={allSelected}
406
408
  indeterminate={someSelected}
407
409
  onChange={(e) => onSelectAll(e.target.checked)}
408
- aria-label={allSelected ? 'Deselect all rows' : 'Select all rows'}
410
+ aria-label={allSelected ? locale.headerDeselectAllLabel : locale.headerSelectAllLabel}
409
411
  tabIndex={-1}
410
412
  onMouseDown={(e) => e.preventDefault()}
411
413
  />
@@ -434,7 +436,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
434
436
  : sortPosition === 0 ? (sortDirection === 'asc' ? 'ascending' : 'descending')
435
437
  : 'none';
436
438
  const sortDescription = isSorted && sortModel.length > 1
437
- ? `Sorted ${sortDirection === 'asc' ? 'ascending' : 'descending'}, sort priority ${sortPosition + 1} of ${sortModel.length}`
439
+ ? locale.headerSortDescription(sortDirection === 'asc' ? 'asc' : 'desc', sortPosition + 1, sortModel.length)
438
440
  : undefined;
439
441
  const isResizable = colDef.resizable !== false;
440
442
  const pinnedPosition = isColumnPinned(colDef.field, pinnedColumns);
@@ -531,7 +533,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
531
533
  type="button"
532
534
  className="ogx__menu-icon-btn"
533
535
  onClick={handleMenuOpen(colDef)}
534
- aria-label={`Open column menu for ${colDef.headerName || colDef.field}`}
536
+ aria-label={locale.headerColumnMenuButtonLabel(colDef.headerName || colDef.field)}
535
537
  aria-haspopup="menu"
536
538
  tabIndex={-1}
537
539
  >
@@ -6,6 +6,7 @@ import { resolveHeaderFilterLocale } from '../../utils/headerFilterLocale';
6
6
  import { getHeaderFilterState, hasHeaderFilter, setHeaderFilterItem } from '../../utils/headerFilters';
7
7
  import { getHeaderColumnStyle } from '../Header/headerColumnStyle';
8
8
  import { HeaderFilterCell } from './HeaderFilterCell';
9
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
9
10
 
10
11
  export interface HeaderFilterRowProps {
11
12
  /** The rendered columns of the column header row (render window with spacers), in render order. */
@@ -68,6 +69,7 @@ function SystemFilterCell({ field, ariaColIndex, sticky, left, focused }: {
68
69
  * through the root-level items it owns (`header:<field>`).
69
70
  */
70
71
  export function HeaderFilterRow(props: HeaderFilterRowProps) {
72
+ const gridLocale = useGridLocale();
71
73
  const {
72
74
  columns, columnWidths, pinnedColumns, pinnedPositions, pinnedEdges, rowReordering, hasDetailPanel,
73
75
  checkboxSelection, pinExpandColumn, pinCheckboxColumn, columnIndexMap, ariaRowIndex, filterModel,
@@ -137,7 +139,7 @@ export function HeaderFilterRow(props: HeaderFilterRowProps) {
137
139
  className={`ogx__header-filter-cell ogx__header-filter-cell--empty ${className}`}
138
140
  style={style}
139
141
  role="columnheader"
140
- aria-label={`No filter for ${colDef.headerName || colDef.field}`}
142
+ aria-label={gridLocale.headerFilterNoFilterLabel(colDef.headerName || colDef.field)}
141
143
  aria-colindex={ariaColIndex}
142
144
  data-field={colDef.field}
143
145
  tabIndex={-1}
@@ -4,6 +4,7 @@ import { ExpandIcon } from '../ui/ExpandIcon';
4
4
  import { Checkbox } from '../ui/Checkbox';
5
5
  import { CellErrorBoundary } from '../Cell/CellErrorBoundary';
6
6
  import { isSyntheticRowId } from '../../utils/syntheticRows';
7
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
7
8
  import type { GridColDef, GridRowModel, GridRowId, GridRowMeta, GridListViewColDef, GridRenderCellParams } from '../../types';
8
9
 
9
10
  export interface ListViewRowProps<R extends GridRowModel = GridRowModel> {
@@ -71,6 +72,7 @@ export function ListViewRow<R extends GridRowModel = GridRowModel>({
71
72
  rowMeta,
72
73
  };
73
74
 
75
+ const locale = useGridLocale();
74
76
  const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
75
77
  e.stopPropagation();
76
78
  onSelectionChange?.(id, e.target.checked);
@@ -96,7 +98,7 @@ export function ListViewRow<R extends GridRowModel = GridRowModel>({
96
98
  {!isSyntheticRow && <Checkbox
97
99
  checked={isSelected}
98
100
  onChange={handleCheckboxChange}
99
- aria-label={`Select row ${id}`}
101
+ aria-label={locale.rowSelectLabel(id)}
100
102
  // Not a Tab stop of its own: the list is one Tab stop, Shift+Space selects the focused row.
101
103
  tabIndex={-1}
102
104
  />}
@@ -3,6 +3,7 @@ import React, { useId } from 'react';
3
3
  import { Button } from '../ui/Button';
4
4
  import { normalizePageSize, getPageCount, clampPage } from '../../utils/pagination';
5
5
  import type { GridLocaleText } from '../../types';
6
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
6
7
 
7
8
  export interface PaginationProps {
8
9
  page: number;
@@ -18,6 +19,7 @@ export interface PaginationProps {
18
19
 
19
20
  export function Pagination(props: PaginationProps) {
20
21
  const pageSizeId = useId();
22
+ const locale = useGridLocale();
21
23
  const {
22
24
  page,
23
25
  pageSize,
@@ -43,15 +45,10 @@ export function Pagination(props: PaginationProps) {
43
45
 
44
46
  const firstRowIndex = currentPage * safePageSize;
45
47
  const lastRowIndex = Math.min(firstRowIndex + safePageSize, rowCount);
46
- const displayedRows = rowCount === 0 ? 0 : `${firstRowIndex + 1}–${lastRowIndex}`;
47
-
48
- const rowsPerPageLabel = localeText?.paginationRowsPerPage ?? 'Rows per page:';
49
- const ofLabel = localeText?.paginationOf
50
- ? localeText.paginationOf(rowCount === 0 ? 0 : firstRowIndex + 1, lastRowIndex, rowCount)
51
- : `${displayedRows} of ${rowCount}`;
52
- const pageLabel = localeText?.paginationPage
53
- ? localeText.paginationPage(currentPage + 1, pageCount)
54
- : `Page ${currentPage + 1} of ${pageCount}`;
48
+ // An explicit `localeText` prop (standalone use) wins over the grid's strings.
49
+ const rowsPerPageLabel = localeText?.paginationRowsPerPage ?? locale.paginationRowsPerPage;
50
+ const ofLabel = (localeText?.paginationOf ?? locale.paginationOf)(rowCount === 0 ? 0 : firstRowIndex + 1, lastRowIndex, rowCount);
51
+ const pageLabel = (localeText?.paginationPage ?? locale.paginationPage)(currentPage + 1, pageCount);
55
52
 
56
53
  const handleFirstPage = () => {
57
54
  onPageChange(0);
@@ -79,7 +76,7 @@ export function Pagination(props: PaginationProps) {
79
76
  const isLastPage = currentPage >= pageCount - 1;
80
77
 
81
78
  return (
82
- <div className="ogx-pagination" role="navigation" aria-label="Pagination">
79
+ <div className="ogx-pagination" role="navigation" aria-label={locale.paginationLabel}>
83
80
  { }
84
81
  <div className="ogx-pagination__page-size">
85
82
  <label htmlFor={pageSizeId} className="ogx-pagination__label">
@@ -112,8 +109,8 @@ export function Pagination(props: PaginationProps) {
112
109
  size="small"
113
110
  onClick={handleFirstPage}
114
111
  disabled={isFirstPage}
115
- aria-label="Go to first page"
116
- title="First page"
112
+ aria-label={locale.paginationFirstPageLabel}
113
+ title={locale.paginationFirstPageTooltip}
117
114
  >
118
115
  <svg className="ogx-pagination__icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
119
116
  <path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6zM6 6h2v12H6z" />
@@ -126,8 +123,8 @@ export function Pagination(props: PaginationProps) {
126
123
  size="small"
127
124
  onClick={handlePreviousPage}
128
125
  disabled={isFirstPage}
129
- aria-label="Go to previous page"
130
- title="Previous page"
126
+ aria-label={locale.paginationPreviousPageLabel}
127
+ title={locale.paginationPreviousPageTooltip}
131
128
  >
132
129
  <svg className="ogx-pagination__icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
133
130
  <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
@@ -143,8 +140,8 @@ export function Pagination(props: PaginationProps) {
143
140
  size="small"
144
141
  onClick={handleNextPage}
145
142
  disabled={isLastPage}
146
- aria-label="Go to next page"
147
- title="Next page"
143
+ aria-label={locale.paginationNextPageLabel}
144
+ title={locale.paginationNextPageTooltip}
148
145
  >
149
146
  <svg className="ogx-pagination__icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
150
147
  <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
@@ -157,8 +154,8 @@ export function Pagination(props: PaginationProps) {
157
154
  size="small"
158
155
  onClick={handleLastPage}
159
156
  disabled={isLastPage}
160
- aria-label="Go to last page"
161
- title="Last page"
157
+ aria-label={locale.paginationLastPageLabel}
158
+ title={locale.paginationLastPageTooltip}
162
159
  >
163
160
  <svg className="ogx-pagination__icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
164
161
  <path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6zM16 6h2v12h-2z" />
@@ -1,6 +1,7 @@
1
1
 
2
2
  import React, { useState, useCallback } from 'react';
3
3
  import { Input } from '../ui/Input';
4
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
4
5
 
5
6
  export interface QuickFilterProps {
6
7
  value?: string;
@@ -13,9 +14,11 @@ export function QuickFilter(props: QuickFilterProps) {
13
14
  const {
14
15
  value = '',
15
16
  onChange,
16
- placeholder = 'Search...',
17
+ placeholder: placeholderProp,
17
18
  debounceMs = 300
18
19
  } = props;
20
+ const locale = useGridLocale();
21
+ const placeholder = placeholderProp ?? locale.toolbarSearchPlaceholder;
19
22
 
20
23
  const [localValue, setLocalValue] = useState(value);
21
24
  const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
@@ -55,7 +58,7 @@ export function QuickFilter(props: QuickFilterProps) {
55
58
  <button
56
59
  className="ogx-quick-filter__clear"
57
60
  onClick={handleClear}
58
- aria-label="Clear search"
61
+ aria-label={locale.toolbarSearchClear}
59
62
  type="button"
60
63
  >
61
64
  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
@@ -5,6 +5,7 @@ import { Checkbox } from '../ui/Checkbox';
5
5
  import { ExpandIcon } from '../ui/ExpandIcon';
6
6
  import { DragHandleIcon } from '../ui/DragHandleIcon';
7
7
  import { DetailPanel } from '../DetailPanel/DetailPanel';
8
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
8
9
  import type { GridRowModel, GridColDef, GridRowId, GridColumnPinning, GridRowPinning, GridRowParams, GridCellParams, GridDetailPanelHeight, GridRowMeta } from '../../types';
9
10
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
10
11
  import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields, isRowPinned } from '../../utils/pinning';
@@ -108,6 +109,7 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
108
109
  }
109
110
 
110
111
  function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
112
+ const locale = useGridLocale();
111
113
  const {
112
114
  row,
113
115
  skeleton = false,
@@ -297,7 +299,7 @@ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
297
299
  <div
298
300
  className={`ogx__cell ogx__cell--drag-handle ${focusedCellField === '__reorder_col__' ? 'ogx__cell--focused' : ''} ${(focusedCellField === '__reorder_col__' && isFocusVisible) ? 'ogx__cell--focus-visible' : ''}`}
299
301
  role="gridcell"
300
- aria-label={onDragStart ? 'Drag to reorder row' : undefined}
302
+ aria-label={onDragStart ? locale.rowReorderLabel : undefined}
301
303
  aria-colindex={reorderColIndex}
302
304
  data-field="__reorder_col__"
303
305
  tabIndex={-1}
@@ -322,7 +324,7 @@ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
322
324
  } ${focusedCellField === '__expand_col__' ? 'ogx__cell--focused' : ''} ${(focusedCellField === '__expand_col__' && isFocusVisible) ? 'ogx__cell--focus-visible' : ''
323
325
  }`}
324
326
  role="gridcell"
325
- aria-label={showDetailPanel ? (isDetailPanelExpanded ? 'Collapse row details' : 'Expand row details') : undefined}
327
+ aria-label={showDetailPanel ? (isDetailPanelExpanded ? locale.rowDetailPanelCollapseLabel : locale.rowDetailPanelExpandLabel) : undefined}
326
328
  aria-expanded={showDetailPanel ? isDetailPanelExpanded : undefined}
327
329
  aria-controls={showDetailPanel && isDetailPanelExpanded ? detailPanelId : undefined}
328
330
  aria-colindex={expandColIndex}
@@ -390,7 +392,7 @@ function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
390
392
  checked={isSelected}
391
393
  onChange={handleCheckboxChange}
392
394
  onClick={(e) => e.stopPropagation()}
393
- aria-label={isSelected ? `Deselect row ${id}` : `Select row ${id}`}
395
+ aria-label={isSelected ? locale.rowDeselectLabel(id) : locale.rowSelectLabel(id)}
394
396
  tabIndex={-1}
395
397
  onMouseDown={(e) => e.preventDefault()}
396
398
  />