Table Workflows
Localization & RTL
Every visible or accessible string of the renderer comes from one messages object, two sets ship, and the locale drives collation and folding. Direction is read from the document unless a prop says otherwise.
- Fixture
- Benchmark
On this page
The renderer contains no literal strings. Labels of the search input, the row and header
checkboxes, the pager buttons, the editor, the column controls and the row-move buttons, the
status lines and the empty text are all keys of TableMessages; three of them are
functions because they take a count or a column name. en is the default and tr ships alongside; a partial messages object overrides any subset.
locale is passed to the Intl.Collator that sorts text and to the case
folding used by search and filters. Folding also normalizes accents and the Turkish dotted and
dotless i, so a search in one form finds the other. Number and date formatting are not part of
the table: a column's format function does it with whatever formatter the application
uses.
import { en, type TableMessages } from '@svelte-lean/table';
// Every key is required in a full set; spread a shipped set to override some.
const de: TableMessages = {
...en,
search: 'Zeilen durchsuchen',
empty: 'Keine passenden Zeilen',
page: (page, pages, rows) => `Seite ${page} von ${pages} · ${rows} Zeilen`,
selected: (count) => `${count} ausgewählt`
};Messages
Replace every built-in message and switch layout direction independently.
Amara Okafor | Engineering | Europe | $4,501 | |
Jonas Lindqvist | Design | Americas | $12,420 | |
Mei Tanaka | Operations | Asia | $20,339 | |
Rafael Duarte | Growth | Europe | $28,258 | |
Selin Aydın | Engineering | Americas | $36,177 |
<script lang="ts">
import { DataTable, en, tr, type TableMessages } from '@svelte-lean/table';
import '@svelte-lean/styles/radio-group.css';
import { memberColumns, pick } from '$lib/demo/columns';
import { people } from '$lib/demo/data';
const data = people(12);
const columns = pick(memberColumns(), ['name', 'team', 'region', 'revenue']);
let language = $state<'en' | 'tr'>('tr');
// A partial override on top of a shipped set: replace one string, keep the rest.
const messages = $derived<Partial<TableMessages>>(
language === 'tr' ? tr : { ...en, empty: 'Nobody matches this search' }
);
</script>
<fieldset data-slean="radio-group">
<legend>Messages</legend>
<label><input type="radio" name="language" value="en" bind:group={language} /> en</label>
<label><input type="radio" name="language" value="tr" bind:group={language} /> tr</label>
</fieldset>
<DataTable
{data}
{columns}
getRowId={(row) => row.id}
{messages}
locale={language === 'tr' ? 'tr-TR' : 'en-US'}
searchable
selection="multiple"
state={{ pageSize: 5 }}
label="Members: localization"
/>Right-to-left
Mirror pinned columns, navigation and resize behavior with one direction prop.
Amara Okafor | Active | Engineering | Europe | $4,501 | 20% | 2026-01-07 |
Jonas Lindqvist | Active | Design | Americas | $12,420 | 33% | 2026-06-18 |
Mei Tanaka | Away | Operations | Asia | $20,339 | 46% | 2026-02-01 |
Rafael Duarte | Invited | Growth | Europe | $28,258 | 59% | 2026-07-12 |
Selin Aydın | Active | Engineering | Americas | $36,177 | 72% | 2026-03-23 |
Noah Brenner | Active | Design | Asia | $44,096 | 85% | 2026-08-06 |
Priya Raman | Away | Operations | Europe | $4,015 | 98% | 2026-04-17 |
Elias Moreau | Invited | Growth | Americas | $11,934 | 30% | 2026-09-28 |
Hana Kowalska | Active | Engineering | Asia | $19,853 | 43% | 2026-05-11 |
Ömer Say | Active | Design | Europe | $27,772 | 56% | 2026-01-22 |
İpek Yılmaz | Away | Operations | Americas | $35,691 | 69% | 2026-06-05 |
Lucas Ferreira | Invited | Growth | Asia | $43,610 | 82% | 2026-02-16 |
<script lang="ts">
import { DataTable } from '@svelte-lean/table';
import { memberColumns } from '$lib/demo/columns';
import { people } from '$lib/demo/data';
const data = people(12);
const columns = memberColumns().map((column) =>
column.id === 'name' ? { ...column, pin: 'left' as const } : { ...column, width: 200 }
);
</script>
<!-- Without a dir prop the table follows the nearest dir attribute; with one it overrides it. -->
<div dir="rtl">
<DataTable {data} {columns} getRowId={(row) => row.id} height="16rem" pagination={false} label="Üyeler" />
</div>
<DataTable dir="rtl" {data} {columns} getRowId={(row) => row.id} label="Üyeler" />- Without a prop the root carries no
dirattribute at all;dir="auto"above it is resolved through:dir(rtl)where supported. - Text alignment uses logical values (
start,end), so numbers stay at the end in both directions. - Pinned columns are placed with
inset-inline-startandinset-inline-end.
API
| Name | Of | Type | Default | Description |
|---|---|---|---|---|
messages | Presentation | Partial<TableMessages> | en | Overrides of the visible and accessible strings; missing keys fall back to en. |
locale | Presentation | string | undefined | Locale of the collator used for sorting and of case folding in search and filters. |
dir | Presentation | 'ltr' | 'rtl' | undefined | Text direction of the table. Without it the root carries no dir attribute and keyboard, resize and pinning follow the nearest dir attribute in the document. |
en | Root entry | TableMessages | – | English messages, the default of the renderer. |
tr | Root entry | TableMessages | – | Turkish messages. |
filter | TableMessages | (column: string) => string | (column) => `Filter ${column}` | Label of a column filter input. |
page | TableMessages | (page: number, pages: number, rows: number | undefined) => string | (page, pages, rows) => `Page ${page} of ${pages}` + (rows === undefined ? '' : ` · ${rows} rows`) | The pager status line; rows is undefined when a manual table has no rowCount. |
selected | TableMessages | (count: number) => string | (count) => `${count} selected` | The live selection count in the toolbar. |
fold | Core entry | (value, locale?) => string | – | Case and accent folding used by search and text filters (Turkish dotted and dotless i included). |