# Migrate wcz-layout v9 to v10

Two headline changes:

1. Login moves to a `/login` page where the user picks a provider. AWS Cognito joins
   Entra. Entra-only apps behave as before, apart from the new page.
2. Every paid MUI X package is gone. `LayoutTable` (TanStack Table v9) replaces
   `DataGridPremium`, pickers move to the free `@mui/x-date-pickers`, and there is no
   licence key any more.

Work through the steps in order.

Commit the migration edits before running `vp check --fix`. On a repo not already
formatted to the current config it rewrites hundreds of unrelated files, and the
middleware arrays you need to eyeball vanish into the churn.

## 1. Dependencies

| Action  | Package                            | Version     |
| ------- | ---------------------------------- | ----------- |
| Install | `@tanstack/react-table`            | 9.x         |
| Install | `@tanstack/react-virtual`          | 3.x         |
| Install | `@tanstack/react-hotkeys`          | 0.x         |
| Install | `@mui/x-date-pickers`              | 9.x         |
| Upgrade | `@tanstack/react-query`            | **≥ 5.102** |
| Upgrade | `@tanstack/react-router-ssr-query` | latest 1.x  |
| Delete  | `@mui/x-data-grid-premium`         | —           |
| Delete  | `@mui/x-date-pickers-pro`          | —           |

```bash
npm install @tanstack/react-table@9 @tanstack/react-virtual@3 @tanstack/react-hotkeys@0 @mui/x-date-pickers@9
npm install @tanstack/react-query@latest @tanstack/react-router-ssr-query@latest
npm uninstall @mui/x-data-grid-premium @mui/x-date-pickers-pro
```

**Both query upgrades are mandatory** and neither failure names a version:

- react-query below 5.102 lacks `QueryKeyWithDataTag`, which v10's `.d.mts` reference.
  Every `wcz-layout/data/client` export then resolves to `unknown`, surfacing as
  `TS18046: 'x' is of type 'unknown'` scattered through your own components.
- ssr-query below 1.167.2 hydrates against the pre-5.102 shape and throws
  `Cannot read properties of undefined (reading 'mutations')` in the browser. No type
  error, so `vp check` passes and the page silently falls back to client rendering.

If `@mui/material` is pinned to an exact version — common, to match the pinned MUI X
Pro packages — loosen it to a caret range, or the install fails `ERESOLVE` against a
newer `@mui/icons-material`.

Then: rewrite every `@mui/x-date-pickers-pro` reference to `@mui/x-date-pickers`,
including the `/// <reference types="@mui/x-date-pickers-pro/themeAugmentation" />`
line atop `src/hooks/useTheme.ts` — not an import, so grepping for one misses it.
Delete every `@mui/x-license` import from `src/router.tsx` and what it feeds —
`LicenseInfo.setLicenseKey`, plus `muiXTelemetrySettings.disableTelemetry()` where it
is used. Drop `VITE_MUI_LICENSE_KEY` from `src/env.ts` and `.env*`, and remove it
from Vault. If that empties the `client` block, delete `clientEnv` and its
import in `src/router.tsx`; delete any `.env*` file left empty.

The range pickers were Pro-only, so `DateRangePicker`, `TimeRangePicker` and
`DateTimeRangePicker` are gone from `useLayoutForm`. Replace each with two adjacent
single-value fields, validate the ordering in the form schema, and delete the
`MuiDateRangePicker` / `MuiDateTimeRangePicker` entries from `useTheme.ts`.

## 2. Renamed auth exports

| v9                                             | v10                     | From                    |
| ---------------------------------------------- | ----------------------- | ----------------------- |
| `requirePermission("key")`                     | `requireAuth("key")`    | `wcz-layout/utils`      |
| `requirePermission("all")`                     | `requireAuth()`         | `wcz-layout/utils`      |
| `authorizationMiddleware("key")`               | `authMiddleware("key")` | `wcz-layout/middleware` |
| `authorizationMiddleware("all")`               | `authMiddleware()`      | `wcz-layout/middleware` |
| `authenticationMiddleware()`                   | `authMiddleware()`      | `wcz-layout/middleware` |
| `authenticationMiddleware({ optional: true })` | dropped, see step 6     |                         |

**The "any employee" key must go.** v9 apps carried a key listing the company-wide
employee groups (`wcz-all-employees`, `wscz-all-employees`) to mean "anyone signed
in". v10 says that with no key at all. Delete the entry from `permissions.ts` —
leaving it breaks AWS, whose sessions carry no groups, so a key-checked guard rejects
every Cognito user.

**Find it by its groups, not its name.** It is usually called `all`, but not always —
one app named it `tester`. Read `permissions.ts` and pick the key whose value is the
company-wide employee groups; that is the one to translate and delete, whatever it is
called. Keys naming a real group (`admin` and friends) stay. Replace any
`hasPermission(user, <that key>)` with `!!user`.

New: `LoginForm` (`wcz-layout/components`), `loginProvidersQueryOptions`
(`wcz-layout`), `Permissions` and `Scopes` types (`wcz-layout/models`), and
`userMiddleware` (`wcz-layout/middleware`) — puts the caller in `context.user`,
`null` when anonymous, requires nobody.

Unchanged: `hasPermission`, `getUser`, `getAccessToken`, `getAppToken`,
`handleLogin`, `handleCallback`, `handleLogout`, `csrfMiddleware`,
`validationMiddleware`. `User` gains an additive `provider` field.

Removed from `wcz-layout/components`: `RouterGridActionsCellItem` and
`EditableColumnHeader` (grid wrappers — row actions now live in a `display` column),
and `TypographyWithIcon` (inline a `Stack direction="row"` with the icon and a
`Typography`).

## 3. src/lib/auth/permissions.ts

```ts
import type { Permissions } from "wcz-layout/models";

export const permissions = {
  admin: ["wcz-developers"],
} as const satisfies Permissions;
```

## 4. src/lib/auth/scopes.ts

Arrays become strings, one scope per key. The object form is only for an API an AWS
session must reach.

```ts
import type { Scopes } from "wcz-layout/models";

export const scopes = {
  graph: "User.Read",
  api: "api://wistron.com/project-name/access_as_user",
  // reachable by both providers:
  // file: { entra: "api://…/access_as_user", aws: "https://…/access_as_user" },
} as const satisfies Scopes;
```

Keep `as const satisfies` on both files — `as const` is what keeps
`requireAuth("admin")` and `getAccessToken("file")` type-checked.

## 5. src/routes/login.tsx, new and required

`requireAuth` redirects unauthenticated users to `/login`; without this file they 404.

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { loginProvidersQueryOptions } from "wcz-layout";
import { LoginForm } from "wcz-layout/components";

export const Route = createFileRoute("/login")({
  loader: ({ context }) => context.queryClient.query(loginProvidersQueryOptions),
  component: LoginForm,
});
```

The loader makes the buttons server-render. `LoginForm` takes no props; it reads
`?returnTo` itself and shows one button per configured provider.

`tsc` will report `'"/login"' is not assignable to 'keyof FileRoutesByPath'` until the
route tree regenerates — run `vp check` before chasing it.

## 6. src/server/middleware/databaseMiddleware.ts

Only if the app has one. The change is one line — swap its auth dependency:

```diff
- .middleware([authenticationMiddleware({ optional: true })])   // or authenticationMiddleware()
+ .middleware([userMiddleware])
```

Keep the shape the app already has. If it is a plain exported middleware object, it
stays one. If it is a factory that existed only to carry an `optional` flag, the
generic factory, the pre-built instances and the overloads all go — there is one
signature now.

### Did it authenticate on its own?

Check what the v9 middleware composed before touching any call site:

- `authenticationMiddleware({ optional: true })` — required nobody. Nothing opens up.
- `authenticationMiddleware()` — **required a signed-in user**, and every call site
  that stood alone inherited that. `userMiddleware` requires nobody, so each of those
  now serves anonymous callers unless you add `authMiddleware()` yourself.

| v9 call site                                               | v9 meaning          | v10                                               |
| ---------------------------------------------------------- | ------------------- | -------------------------------------------------- |
| `databaseMiddleware({ optional: true })`                   | anonymous allowed   | `[databaseMiddleware()]`                          |
| `databaseMiddleware()` over `authenticationMiddleware()`   | user required       | `[databaseMiddleware(), authMiddleware()]`        |
| `[authorizationMiddleware("admin"), databaseMiddleware()]` | permission required | `[databaseMiddleware(), authMiddleware("admin")]` |

Grep the middleware name, not `createServerFn` — route `server.middleware` arrays
count too, and job, cron and webhook endpoints are where a lone `databaseMiddleware`
hides. Those are the worst ones to leave anonymous.

### Order: `databaseMiddleware` first

Middleware context merges left to right and the later entry wins, so the order
decides whether `context.user` is nullable in the handler. `databaseMiddleware`
declares `userMiddleware` and contributes `user: User | null`; `authMiddleware`
contributes `user: User`. Put the database one first and the narrowed type survives:

```ts
.middleware([databaseMiddleware(), authMiddleware("admin")])
.handler(({ context }) => context.user.name) // User, no narrowing needed
```

v9 arrays are usually written the other way round, so reorder them as you go. Any
`validationMiddleware` in a REST route still goes **after** `authMiddleware`, or an
unauthenticated caller gets their body parsed and your validation errors back:
`[databaseMiddleware(), authMiddleware("admin"), validationMiddleware(Schema)]`.

## 7. src/start.ts

v10 apps need no `src/start.ts` just for CSRF. When the file is absent, TanStack Start
installs its own default request middleware,
`createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" })` — exactly
what `csrfMiddleware` from `wcz-layout/middleware` is.

Open the file and read what `createStart` returns:

- **Only `requestMiddleware: [csrfMiddleware]`**, with `csrfMiddleware` imported from
  `wcz-layout/middleware` and no other option → delete `src/start.ts` completely.
- **Anything else** — a second entry in `requestMiddleware`, a `functionMiddleware`
  array, any other option, or a `createCsrfMiddleware` of the app's own with a
  different filter → keep the file as it is.

After deleting, `src/routeTree.gen.ts` still imports `startInstance` from `./start.ts`
until the route tree regenerates — run `vp check` before chasing it. Nothing else
should import `startInstance`; grep to confirm.

## 8. Locales

The app owns every string — the vite plugin builds i18next resources from the app's
`src/lib/locales/*.json`, so a key the library renders but the app does not define
comes out raw. The package does not ship its locale files: the complete v10 `Layout`
block for `en` and `cs` is at the end of this guide, under
[Appendix: v10 Layout locales](#appendix-v10-layout-locales).

v10 adds the whole `Layout.Table` namespace plus `Layout.ContinueWith` and
`Layout.TestEnvironment`. Take only the `Layout` key; nothing else in the app's
locale files is the library's.

**Merge it into the app's `Layout`, do not replace the block.** Apps add their own
keys inside that namespace (`Layout.Loading`, `Layout.NotProvided` in one app), and a
wholesale replace silently deletes them — which *does* break the build, because
`src/types/i18next.d.ts` types `t()` from `en.json` and the app's own
`t("Layout.Loading")` calls stop compiling. So: overlay the library's keys on top of
the app's, then look at what is left over. A leftover key the app's own code
references is the app's — keep it. One nothing references is a v9 library key v10
dropped — remove it, or it lingers forever because it passes every check.

**Use the block from the guide of the major you are migrating to.** Do this step
again in every major's pass: `Layout` keys change between majors, and each guide's
appendix holds its own major's set. A v9 → v11 upgrade runs the v10 pass with this
appendix and the v11 pass with the v11 guide's. Carrying v10's block into a v11 app
leaves the wrong key set.

**A library key the app never calls can go missing without failing the build.**
`t()` is typed from the app's `en.json`, so it catches the app's own calls and nothing
else — the library's are compiled already. A `Layout.Table.*` key the app never
references therefore survives `vp check` and surfaces only as a raw key string on
screen. Verify deliberately: compare the app's `Layout` key set against the appendix,
and look at the login card and the table toolbar, where a miss shows first.

**Every language the app ships needs the new keys, not just the two.** The appendix
covers `en` and `cs`; an app may have any number of files in `src/lib/locales/`. For
every other file (`de.json`, `pl.json`, `zh.json`, …), take the **English** block and
translate it into that file's language yourself, as part of this step — do not ask,
do not leave English values in place, do not skip the file. Nobody else will, and the
gap is invisible until someone switches language.

- Merge the same way as above: keys the app already translated keep their existing
  values; translate only the keys the file is missing.
- Translate values only. Keys, `{{provider}}` / `{{name}}` / `{{count}}`
  interpolations and the `<env>` tag stay byte-for-byte.
- Emit the plural suffixes the target language needs, not English's. English has
  `_one` / `_other`, Czech adds `_few`, Polish needs `_one` / `_few` / `_many` /
  `_other`, and Chinese or Japanese only `_other`. When unsure, check
  `new Intl.PluralRules("<lang>").resolvedOptions().pluralCategories`; a category
  that only applies to fractions (Czech `many`) can be left out, `count` is whole.
- Match the tone of the file's existing strings (formal vs. informal address).

`Layout.TestEnvironment` holds an `<env>` tag the mascot renders through `Trans` —
keep the tag in every language, it colours the word.

## 9. Replace the grid

**Only where `DataGridPremium` is used.** A table already built on TanStack Table
stays exactly as written — do not convert it to `LayoutTable`. This step exists
because the paid grid is gone, not because `LayoutTable` is the house table for
everything. Grep for `DataGridPremium`; anything else is out of scope.

Read the `table` skill before writing any columns — it documents the API. This is
only the mapping.

Delete `src/components/DataGridToolbar.tsx`; `LayoutTable` renders the toolbar,
quick filter, columns panel and filter panel itself. In `useTheme.ts`, drop
`import { csCZ, enUS } from "@mui/x-data-grid/locales"` and its two entries.

| DataGridPremium                                     | LayoutTable                                                   |
| --------------------------------------------------- | ------------------------------------------------------------- |
| `Array<GridColDef<Row>>`                            | `columnHelper.columns([...])` from `createLayoutColumnHelper` |
| `field` / `headerName`                              | the accessor key / `header`                                   |
| `width`, `flex`, `minWidth`                         | `size`                                                        |
| `renderCell`                                        | `cell: ({ cell }) => <cell.ValueCell />`                      |
| `valueGetter: (_v, row) => row.x.y`                 | `columnHelper.accessor((row) => row.x.y, { id: "..." })`      |
| `align` / `headerAlign`                             | `meta: { align }`                                             |
| `initialState.pinnedColumns.left` / `.right`        | `initialState.columnPinning.start` / `.end`                   |
| `initialState.sorting.sortModel` `{field, sort}`    | `initialState.sorting` `[{ id, desc }]`                       |
| `valueFormatter` on a date column                   | `cell.DateCell` or `cell.DateTimeCell`                        |
| `valueFormatter` with an `Intl.NumberFormat`        | `cell.NumberCell options={...}`, same options object          |
| `type: "singleSelect"` with `valueOptions`          | `meta: { variant: "select", options }`                        |
| `type: "number"` / `"date"` / `"boolean"`           | `meta: { variant: "number" \| "date" \| "boolean" }`          |
| `rows` and `columns` props                          | `useLayoutTable({ data, columns })`, then `table={table}`     |
| `showToolbar` with `slots.toolbar`                  | `showToolbar` with `title` and `actions` on `LayoutTable`     |
| `ignoreDiacritics`                                  | always on                                                     |
| `rowSelectionModel` state                           | `table.getSelectedRowIds()`, `table.resetRowSelection()`      |
| `cellSelection`                                     | always on, `Mod+C` copies the range as TSV                    |
| `GridActionsCellItem` in a `type: "actions"` column | a `display` column rendering `cell.ActionsCell`               |

**Every row needs an `id`.** `getRowId` is not an option — the library hardcodes
`row.id`. A table over a view or a natural-key table without an `id` column must add
one where the rows are built (`id: row.cpn`), and the row type gains `id: string`.

**Dropped with no replacement**, so decide per case whether to rebuild or drop:
`getRowClassName` and `cellClassName` (style inside the cell instead — a `sx` on
`cell.NumberCell` colours the text, nothing reaches the row), `rowHeight`, the
Excel export button, and `localeText` overrides such as `footerTotalRows`. Any `sx`
block targeting `.MuiDataGrid-*` classes is dead: delete it rather than translating
it, LayoutTable's DOM is unrelated.

**State persistence changes shape.** `apiRef.exportState()` / `restoreState()` and the
per-change callbacks (`onSortModelChange`, `onPinnedColumnsChange`, …) are gone.
Restore by passing `initialState` read from storage before first render, and persist
with one effect on `table.state`. There is no `onStateChange` option.

**Shared generic column factories do not port.** A v9 helper like
`actionColumn<T>(): GridColDef<T>` reused across pages cannot be rewritten as
`(helper: ColumnHelper<T>) => ...` — `createLayoutColumnHelper` is invariant in its row
type, so a concrete helper will not satisfy a generic parameter. Share the *contents*
instead: a function returning `Array<RowAction<T>>` (that type is generic-friendly) or
a plain predicate, and let each page write its own five-line `display` column.

**Inline editing has no equivalent.** No `editMode`, no `processRowUpdate`, no
editable columns, and `EditableColumnHeader` is gone. A page built on
`editMode="row"` is a redesign, not a column mapping: move create and edit into a
dialog with a `useLayoutForm` and leave the table read-only. Usually the largest item
in the whole migration. It also retires `rowModesModel`, the `isNew` row flag and any
"turn the Zod error into a string for the grid" helper — delete them, the form renders
field errors itself.

**Bulk actions need a selection column.** A v9 page driving actions off
`Object.keys(cellSelectionModel)` has nothing to map onto: cell selection is always on
in v10 but exists for `Mod+C`. `table.getSelectedRowIds()` reads *row* selection,
which renders only if the columns start with a `select` display column
(`header.SelectAllCell` / `cell.SelectCell`, `size: 50`, `enableResizing: false`,
`enableCellSelection: false`). Ids come back as plain strings, so the `id.toString()`
that `GridRowId` forced can go.

Two behaviours that catch people out. `meta.variant: "select"` and `"multi-select"`
read options from the column's faceted unique values, so pass `meta.options` only when
the label differs from the stored value — a translated database enum, derived from
`enumValues`. And grouping shows a value only for the grouping column and columns that
resolve an aggregation function (a number or `Date`, unless the column sets
`aggregationFn`); others stay blank in a group row.

## 10. useNotification

`useNotification` is gone, along with its snackbar and the `snackbarOrigin` layout
option. Replace every call with something the page renders itself — a success
animation, a non-dismissing alert dialog for errors. Match whatever the app already
does for feedback rather than introducing a new pattern.

Two things to check:

- A `notify(..., { severity: "error" })` sitting next to an `alert(error.message)`
  from `useDialogs` is redundant. Delete the `notify`, keep the `alert`.
- A success `notify` that fires **after** a `navigate` cannot become a component on
  the page it was called from — that page has already unmounted. Invert it: render
  the feedback, and navigate when it finishes.

## 11. AWS Cognito, optional

Skip for Entra-only apps. To offer AWS, add to the `server` block of `src/env.ts`:

```ts
AWS_ISSUER: z.url().optional(),          // https://cognito-idp.<region>.amazonaws.com/<poolId>
AWS_DOMAIN: z.url().optional(),          // https://<prefix>.auth.<region>.amazoncognito.com
AWS_CLIENT_ID: z.string().min(1).optional(),
AWS_CLIENT_SECRET: z.string().min(1).optional(),
```

Set all four in `.env.local`; the second button appears only when all are present.
The Cognito app client must be confidential, use the authorization-code grant, and
list `<origin>/auth/callback` as callback and `<origin>/` as sign-out URL.

An AWS session requests OIDC scopes only, so it cannot mint a delegated API token.
Cognito cannot widen scopes after login, so adding a downstream API means requesting
every `aws` scope in `scopes.ts` at login, and an unregistered one fails the whole
sign-in — register them all on the app client first. Downstream APIs must also accept
the Cognito JWKS and authorize on the token's `scope` claim, since Cognito access
tokens carry no `aud`.

## Appendix: v10 Layout locales

The complete `Layout` block wcz-layout v10 renders, for step 8. Merge it into the
app's `src/lib/locales/en.json` and `cs.json` as that step describes — do not paste
it over the app's block. For any other language, translate the English block.

### en.json

```json
{
  "Layout": {
    "LogIn": "Log In",
    "ContinueWith": "Continue with {{provider}}",
    "Logout": "Logout",
    "Language": "Language",
    "Appearance": "Appearance",
    "Light": "Light",
    "Dark": "Dark",
    "System": "System",
    "ThisPageCouldNotBeFound": "This page could not be found.",
    "Settings": "Settings",
    "Unauthorized": "Unauthorized",
    "TestEnvironment": "You’re in the <env>TEST</env> environment",
    "Dialog": {
      "Confirm": "Confirm",
      "Cancel": "Cancel",
      "Alert": "Alert"
    },
    "File": {
      "AreYouSureYouWantToDelete": "Are you sure you want to delete {{name}}?",
      "DragSomeFilesHereOrClickToSelectThem": "Drag some files here or click to select them",
      "Delete": "Delete",
      "Download": "Download"
    },
    "Table": {
      "Search": "Search",
      "ClearSearch": "Clear search",
      "ColumnMenu": "Column menu",
      "Filters": "Filters",
      "AddFilter": "Add filter",
      "RemoveFilter": "Remove filter",
      "ClearFilters": "Clear filters",
      "And": "and",
      "Or": "or",
      "Field": "Field",
      "Operator": "Operator",
      "Value": "Value",
      "NoValueRequired": "No value required",
      "From": "From",
      "To": "To",
      "DaysFromToday": "Days from today",
      "NoRows": "No rows",
      "SortAscending": "Sort ascending",
      "SortDescending": "Sort descending",
      "ClearSort": "Clear sort",
      "PinStart": "Pin to start",
      "PinEnd": "Pin to end",
      "Unpin": "Unpin",
      "GroupBy": "Group by this column",
      "Ungroup": "Stop grouping",
      "RowActions": "Row actions",
      "SelectedCount_one": "{{count}} selected",
      "SelectedCount_other": "{{count}} selected",
      "MoveLeft": "Move left",
      "MoveRight": "Move right",
      "All": "All",
      "Columns": "Columns",
      "ShowHideAll": "Show/Hide all",
      "HideColumn": "Hide column",
      "Operators": {
        "includesString": "contains",
        "notIncludesString": "does not contain",
        "equalsString": "is",
        "notEqualsString": "is not",
        "startsWith": "starts with",
        "endsWith": "ends with",
        "isEmpty": "is empty",
        "isNotEmpty": "is not empty",
        "equals": "is",
        "notEquals": "is not",
        "greaterThan": "is greater than",
        "greaterThanOrEqualTo": "is greater than or equal to",
        "lessThan": "is less than",
        "lessThanOrEqualTo": "is less than or equal to",
        "inRange": "is between",
        "isRelativeToToday": "is relative to today",
        "Date": {
          "lessThan": "is before",
          "lessThanOrEqualTo": "is on or before",
          "greaterThan": "is after",
          "greaterThanOrEqualTo": "is on or after"
        }
      }
    }
  }
}
```

### cs.json

```json
{
  "Layout": {
    "LogIn": "Přihlásit se",
    "ContinueWith": "Pokračovat přes {{provider}}",
    "Logout": "Odhlásit se",
    "Language": "Jazyk",
    "Appearance": "Vzhled",
    "Light": "Světlý",
    "Dark": "Tmavý",
    "System": "Systém",
    "ThisPageCouldNotBeFound": "Tato stránka nebyla nalezena.",
    "Settings": "Nastavení",
    "Unauthorized": "Neoprávněný",
    "TestEnvironment": "Jste v <env>TESTOVACÍM</env> prostředí",
    "Dialog": {
      "Confirm": "Potvrdit",
      "Cancel": "Zrušit",
      "Alert": "Upozornění"
    },
    "File": {
      "AreYouSureYouWantToDelete": "Opravdu chcete smazat {{name}}?",
      "DragSomeFilesHereOrClickToSelectThem": "Přetáhněte sem některé soubory nebo je vyberte kliknutím",
      "Delete": "Smazat",
      "Download": "Stáhnout"
    },
    "Table": {
      "Search": "Hledat",
      "ClearSearch": "Zrušit hledání",
      "ColumnMenu": "Nabídka sloupce",
      "Filters": "Filtry",
      "AddFilter": "Přidat filtr",
      "RemoveFilter": "Odebrat filtr",
      "ClearFilters": "Zrušit filtry",
      "And": "a",
      "Or": "nebo",
      "Field": "Pole",
      "Operator": "Operátor",
      "Value": "Hodnota",
      "NoValueRequired": "Hodnota není potřeba",
      "From": "Od",
      "To": "Do",
      "DaysFromToday": "Dnů od dneška",
      "NoRows": "Žádné řádky",
      "SortAscending": "Seřadit vzestupně",
      "SortDescending": "Seřadit sestupně",
      "ClearSort": "Zrušit řazení",
      "PinStart": "Připnout na začátek",
      "PinEnd": "Připnout na konec",
      "Unpin": "Odepnout",
      "GroupBy": "Seskupit podle sloupce",
      "Ungroup": "Zrušit seskupení",
      "RowActions": "Akce řádku",
      "SelectedCount_one": "{{count}} vybraný",
      "SelectedCount_few": "{{count}} vybrané",
      "SelectedCount_other": "{{count}} vybraných",
      "MoveLeft": "Posunout doleva",
      "MoveRight": "Posunout doprava",
      "All": "Vše",
      "Columns": "Sloupce",
      "ShowHideAll": "Zobrazit/skrýt vše",
      "HideColumn": "Skrýt sloupec",
      "Operators": {
        "includesString": "obsahuje",
        "notIncludesString": "neobsahuje",
        "equalsString": "je",
        "notEqualsString": "není",
        "startsWith": "začíná na",
        "endsWith": "končí na",
        "isEmpty": "je prázdné",
        "isNotEmpty": "není prázdné",
        "equals": "je",
        "notEquals": "není",
        "greaterThan": "je větší než",
        "greaterThanOrEqualTo": "je větší nebo rovno",
        "lessThan": "je menší než",
        "lessThanOrEqualTo": "je menší nebo rovno",
        "inRange": "je mezi",
        "isRelativeToToday": "je relativní ke dnešku",
        "Date": {
          "lessThan": "je před",
          "lessThanOrEqualTo": "je nejpozději",
          "greaterThan": "je po",
          "greaterThanOrEqualTo": "je nejdříve"
        }
      }
    }
  }
}
```
