Filter table with client side selection (#4616)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brett Jephson
2026-09-22 09:28:07 +01:00
committed by GitHub
parent 357af3f6d2
commit 0f0cfde6ba
44 changed files with 596 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Narrow a table or card grid to the reader's content selection: choosing an option in a tab or a select button now filters any select column that offers it. The table says what it has been narrowed to, and offers a Clear that drops the selection.
@@ -15,7 +15,12 @@ import {
} from './search';
import { shouldShowTableSearch } from './shouldShowSearch';
import { StickyViewGrid } from './StickyViewGrid';
import { TableSearchEmpty, TableSearchInput, TableSearchProvider } from './TableSearch';
import {
TableSearchEmpty,
TableSearchInput,
TableSearchProvider,
TableSelectionFilter,
} from './TableSearch';
import { ViewCards } from './ViewCards';
import { ViewGrid, ViewGridHeader } from './ViewGrid';
import { tcls } from '@/lib/tailwind';
@@ -48,9 +53,14 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
searchOverride: block.data.search,
isPrint: context.mode === 'print',
});
const searchRecords = showSearch
? records.map(([id, record]) => ({ id, ...getTableRecordSearchData(block, record) }))
: [];
const selectColumns = getTableSelectColumns(block);
// Also needed when the search bar is hidden: a reader's content selection can narrow a select
// column from a tab or picker elsewhere on the page, and without records there is nothing to
// match against. A table with no select column can never be narrowed that way, so it skips.
const searchRecords =
showSearch || selectColumns.length > 0
? records.map(([id, record]) => ({ id, ...getTableRecordSearchData(block, record) }))
: [];
const cellMergeLayout = createTableCellMergeLayout(
block,
records.map(([recordId]) => recordId)
@@ -59,6 +69,7 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
return (
<TableSearchProvider
records={searchRecords}
selectColumns={selectColumns}
recordGroups={
block.data.view.type === 'grid' ? cellMergeLayout.recordGroups : undefined
}
@@ -66,10 +77,14 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
<div className={tcls(style, 'flex flex-col gap-3')}>
{showSearch ? (
<TableSearchInput
selectColumns={getTableSelectColumns(block)}
selectColumns={selectColumns}
checkboxColumns={getTableCheckboxColumns(block)}
/>
) : null}
{/* Sits under the filter control it relates to, and tight against the search bar,
so it reads as part of the filter controls rather than a caption on the table.
Standalone — cards, short grids — there is no control to sit under. */}
<TableSelectionFilter className={showSearch ? '-mt-1.5 justify-end' : undefined} />
<TableView
{...props}
isOffscreen={isOffscreen}
@@ -10,8 +10,16 @@ import {
type TableSearchRecordData,
getVisibleTableRecordIds,
} from './searchMatch';
import {
type SlugFilterEntry,
getAppliedSlugFilter,
reconcileSelectedOptions,
resolveSlugFilter,
slugFilterKey,
} from './slugFilter';
import { Button, Checkbox, DropdownMenu, DropdownMenuItem, Input } from '@/components/primitives';
import { tString, useLanguage } from '@/intl/client';
import { selectStore } from '@/lib/select';
import { type ClassValue, tcls } from '@/lib/tailwind';
/**
@@ -37,6 +45,13 @@ type TableSearchContextValue = {
visibleIds: ReadonlySet<string> | null;
/** True when there are records but the active filters match none of them. */
isEmpty: boolean;
/**
* Select columns the reader's content selection narrowed and the reader has left alone.
* A column they have since filtered themselves drops out, since the table no longer shows it.
*/
slugFilter: SlugFilterEntry[];
/** Drop the selection driving {@link slugFilter}, site-wide. The only thing that clears one. */
clearSlugFilter: () => void;
};
const TableSearchContext = React.createContext<TableSearchContextValue | null>(null);
@@ -47,16 +62,32 @@ const TableSearchContext = React.createContext<TableSearchContextValue | null>(n
export function TableSearchProvider(props: {
records?: TableSearchRecordData[];
recordGroups?: readonly (readonly string[])[];
/** Select columns of this table, so the reader's content selection can narrow them. */
selectColumns?: TableSelectColumn[];
children: React.ReactNode;
}) {
const { records = [], recordGroups = [] } = props;
const { records = [], recordGroups = [], selectColumns = [] } = props;
const [query, setQuery] = React.useState('');
const [selectedOptions, setSelectedOptions] = React.useState<SelectedOptions>(() => ({}));
const [checkedColumns, setCheckedColumns] = React.useState<ReadonlySet<string>>(
() => new Set()
);
// The selection that drives this lives outside the table — a tab, a select button or a picker
// elsewhere on the page — so this synchronises with it rather than deriving from it.
const slugFilter = useSlugFilter(selectColumns);
// Columns the selection narrowed last time round. Kept so a new selection — or clearing it —
// undoes the previous one, rather than leaving a filter the reader can no longer account for.
const narrowedColumns = React.useRef<string[]>([]);
const toggleOption = React.useCallback((column: string, value: string) => {
// The reader is taking this column over, so the selection no longer owns it: a later clear
// must leave their choice of options alone. Their change stays local — the filter is this
// table's, for this visit, while the selection is site-wide and persists, so only the
// clear beside the notice touches it.
narrowedColumns.current = narrowedColumns.current.filter((narrowed) => narrowed !== column);
setSelectedOptions((previous) => {
const values = new Set(previous[column]);
if (values.has(value)) {
@@ -87,6 +118,33 @@ export function TableSearchProvider(props: {
});
}, []);
React.useEffect(() => {
const previouslyNarrowed = narrowedColumns.current;
narrowedColumns.current = slugFilter.map((entry) => entry.column);
setSelectedOptions((previous) =>
reconcileSelectedOptions(previous, previouslyNarrowed, slugFilter)
);
}, [slugFilter]);
// What the notice may speak for: the columns the selection narrowed and the reader has left
// alone. A column they have since filtered themselves still has an active slug, but the table
// is no longer showing it, so the notice must not claim it.
const appliedSlugFilter = React.useMemo(
() => getAppliedSlugFilter(slugFilter, selectedOptions),
[slugFilter, selectedOptions]
);
// Clearing goes through the store rather than local state: the selection is what persists, so
// only dropping it there stops the filter coming back on the next load. It is site-wide, so a
// tab elsewhere on the page reverts to its default too. This is the only thing that clears a
// selection — changing the filter never does.
const clearSlugFilter = React.useCallback(() => {
for (const entry of appliedSlugFilter) {
selectStore.deactivate(entry.slug);
}
}, [appliedSlugFilter]);
// Match every record once, here, rather than in each row — rows just look themselves up by id.
const visibleIds = React.useMemo(
() =>
@@ -112,8 +170,20 @@ export function TableSearchProvider(props: {
toggleCheckbox,
visibleIds,
isEmpty,
slugFilter: appliedSlugFilter,
clearSlugFilter,
}),
[query, selectedOptions, toggleOption, checkedColumns, toggleCheckbox, visibleIds, isEmpty]
[
query,
selectedOptions,
toggleOption,
checkedColumns,
toggleCheckbox,
visibleIds,
isEmpty,
appliedSlugFilter,
clearSlugFilter,
]
);
return (
@@ -121,6 +191,40 @@ export function TableSearchProvider(props: {
);
}
/**
* The reader's selection, reduced to the columns of *this* table.
*
* Subscribes once and returns a string rather than an object: `useSyncExternalStore` compares
* snapshots by identity, so a fresh object each call would loop. It also means a selection that
* changes nothing for this table re-renders nothing — the reason `useSelect` stopped exposing the
* recency list in the first place.
*/
function useSlugFilter(selectColumns: TableSelectColumn[]): SlugFilterEntry[] {
const columnsKey = selectColumns
.map(
(column) =>
`${column.id}:${column.options.map((option) => `${option.value}=${option.label}`).join('|')}`
)
.join(';');
const getKey = React.useCallback(
() => slugFilterKey(resolveSlugFilter(selectColumns, selectStore.getState().slugs)),
// `selectColumns` is a fresh array each render; its contents are what matter.
// oxlint-disable-next-line react-hooks/exhaustive-deps
[columnsKey]
);
const filterKey = React.useSyncExternalStore(selectStore.subscribe, getKey, getKey);
// The key is only an identity: rebuild the filter itself when it moves, rather than parsing the
// key back apart, since an option value can be any string an import gave it.
return React.useMemo(
() => resolveSlugFilter(selectColumns, selectStore.getState().slugs),
// oxlint-disable-next-line react-hooks/exhaustive-deps
[filterKey]
);
}
function useTableSearch(): TableSearchContextValue {
const context = React.useContext(TableSearchContext);
if (!context) {
@@ -193,6 +297,49 @@ export function TableSearchEmpty(props: { className?: ClassValue }) {
);
}
/**
* Names the selection narrowing this table, and lets the reader drop it.
*
* Deliberately worded around the *selection* rather than the filter: the column dropdown beside it
* shows the same column as active, but clearing there only resets local state and the filter returns
* on the next load. This is the control that actually undoes it.
*
* Rendered independently of the search bar. `shouldShowTableSearch` leaves the filter controls off
* cards, off grids below the row threshold, and off any table whose author turned search off — and
* in every one of those a narrowed table would otherwise just read as missing rows.
*/
export function TableSelectionFilter(props: { className?: ClassValue }) {
const language = useLanguage();
const { slugFilter, clearSlugFilter } = useTableSearch();
if (slugFilter.length === 0) {
return null;
}
return (
<div
className={tcls('flex flex-wrap items-center gap-2 text-sm text-tint', props.className)}
>
<Icon icon="filter" className="size-3 shrink-0" />
<span>
{tString(
language,
'table_filtered_by_selection',
slugFilter.map((entry) => entry.label).join(', ')
)}
</span>
<Button
variant="blank"
size="xsmall"
icon="xmark"
iconOnly
label={tString(language, 'table_clear_selection')}
onClick={clearSlugFilter}
/>
</div>
);
}
/**
* A blank multi-select dropdown button for a single select column. Becomes `active` while any
* of its options are selected.
@@ -0,0 +1,217 @@
import { describe, expect, it } from 'bun:test';
import type { TableSelectColumn } from './search';
import {
getAppliedSlugFilter,
getOptionSlug,
reconcileSelectedOptions,
resolveSlugFilter,
slugFilterKey,
} from './slugFilter';
/**
* A select column as the editor writes one: each option carries an opaque generated `value` and the
* author's wording as its `label`. Tests must not conflate the two — matching on `value` looks
* right against fixtures that reuse the label and matches nothing against real content.
*/
function column(id: string, labels: string[]): TableSelectColumn {
return {
id,
label: id,
options: labels.map((label, index) => ({
value: `${id}-key-${index}`,
label,
color: 'blue',
})),
};
}
/** The opaque value of the option an author labelled `label`. */
function valueOf(column: TableSelectColumn, label: string): string {
const option = column.options.find((option) => option.label === label);
if (!option) {
throw new Error(`no option labelled ${label}`);
}
return option.value;
}
describe('resolveSlugFilter', () => {
const platform = column('platform', ['macOS', 'Windows', 'Linux']);
const status = column('status', ['Done', 'To do']);
it('matches the option label, and resolves to its opaque value', () => {
expect(resolveSlugFilter([platform], ['macos'])).toEqual([
{
column: 'platform',
value: valueOf(platform, 'macOS'),
label: 'macOS',
slug: 'macos',
},
]);
});
it('carries the label and slug needed to show and clear the filter', () => {
// The reader is told what narrowed the table, and the slug is what clearing deactivates.
const [entry] = resolveSlugFilter([platform], ['windows']);
expect(entry?.label).toBe('Windows');
expect(entry?.slug).toBe('windows');
});
it('never matches the opaque value itself', () => {
expect(resolveSlugFilter([platform], [valueOf(platform, 'macOS')])).toEqual([]);
});
it('slugifies the label the way every other select surface does', () => {
const languages = column('language', ['Node.js', 'C++', 'Windows 10']);
expect(resolveSlugFilter([languages], ['node.js'])[0]?.label).toBe('Node.js');
expect(resolveSlugFilter([languages], ['c++'])[0]?.label).toBe('C++');
expect(resolveSlugFilter([languages], ['windows-10'])[0]?.label).toBe('Windows 10');
});
it('falls back to the value when an option has no label', () => {
const legacy: TableSelectColumn = {
id: 'legacy',
label: 'legacy',
options: [{ value: 'macos', label: '', color: 'blue' }],
};
expect(resolveSlugFilter([legacy], ['macos'])).toEqual([
{ column: 'legacy', value: 'macos', label: 'macos', slug: 'macos' },
]);
});
it('leaves a column the selection says nothing about', () => {
const entries = resolveSlugFilter([platform, status], ['macos']);
expect(entries.map((entry) => entry.column)).toEqual(['platform']);
});
it('narrows several columns at once when the selection covers both', () => {
const entries = resolveSlugFilter([platform, status], ['macos', 'done']);
expect(entries.map((entry) => entry.value)).toEqual([
valueOf(platform, 'macOS'),
valueOf(status, 'Done'),
]);
});
it('takes the most recently activated when a column offers several active options', () => {
// Most-recent-first, so `windows` wins over `macos` — the rule tabs resolve with.
expect(resolveSlugFilter([platform], ['windows', 'macos'])[0]?.value).toBe(
valueOf(platform, 'Windows')
);
expect(resolveSlugFilter([platform], ['macos', 'windows'])[0]?.value).toBe(
valueOf(platform, 'macOS')
);
});
it('filters nothing when the selection matches no column', () => {
expect(resolveSlugFilter([platform, status], ['python'])).toEqual([]);
expect(resolveSlugFilter([platform], [])).toEqual([]);
expect(resolveSlugFilter([], ['macos'])).toEqual([]);
});
});
describe('getOptionSlug', () => {
// Shared by the matcher and by the write-back that moves the selection when a reader changes a
// governed column, so the two can never disagree about what an option answers to.
it('slugifies the label, and falls back to the value', () => {
expect(getOptionSlug({ value: 'key-0', label: 'macOS' })).toBe('macos');
expect(getOptionSlug({ value: 'key-1', label: 'Windows 10' })).toBe('windows-10');
expect(getOptionSlug({ value: 'macos', label: '' })).toBe('macos');
});
});
describe('reconcileSelectedOptions', () => {
const entry = (column: string, value: string) => ({
column,
value,
label: value,
slug: value,
});
it('drops the column when the selection is cleared', () => {
const previous = { platform: new Set(['macos-value']) };
expect(reconcileSelectedOptions(previous, ['platform'], [])).toEqual({});
});
it('leaves filters the reader set themselves', () => {
const previous = {
platform: new Set(['macos-value']),
status: new Set(['done-value']),
};
expect(reconcileSelectedOptions(previous, ['platform'], [])).toEqual({
status: new Set(['done-value']),
});
});
it('replaces the previous selection rather than adding to it', () => {
const previous = { platform: new Set(['macos-value']) };
expect(
reconcileSelectedOptions(previous, ['platform'], [entry('platform', 'windows-value')])
).toEqual({ platform: new Set(['windows-value']) });
});
it('gives the same answer however many times it is applied', () => {
// React may invoke a state updater more than once with the same input. An earlier version
// tracked the narrowed columns inside the updater, so the second pass saw them already
// cleared, took the early return and handed back the *unchanged* state — silently undoing
// a clear while leaving an apply working, which is exactly how it presented.
const previous = { platform: new Set(['macos-value']) };
const once = reconcileSelectedOptions(previous, ['platform'], []);
const twice = reconcileSelectedOptions(previous, ['platform'], []);
expect(twice).toEqual(once);
});
it('is a no-op when there is nothing to narrow and nothing to undo', () => {
const previous = { status: new Set(['done-value']) };
expect(reconcileSelectedOptions(previous, [], [])).toBe(previous);
});
});
describe('getAppliedSlugFilter', () => {
const macos = { column: 'platform', value: 'macos-value', label: 'macOS', slug: 'macos' };
it('speaks for a column the reader has left alone', () => {
const selected = { platform: new Set(['macos-value']) };
expect(getAppliedSlugFilter([macos], selected)).toEqual([macos]);
});
it('drops a column the reader has filtered to something else', () => {
// Changing the filter deliberately leaves the selection active, so the slug is still there;
// the notice just must not claim a match the table is no longer showing.
const selected = { platform: new Set(['windows-value']) };
expect(getAppliedSlugFilter([macos], selected)).toEqual([]);
});
it('drops a column the reader has widened to several options', () => {
const selected = { platform: new Set(['macos-value', 'windows-value']) };
expect(getAppliedSlugFilter([macos], selected)).toEqual([]);
});
it('drops a column the reader has cleared', () => {
expect(getAppliedSlugFilter([macos], {})).toEqual([]);
});
});
describe('slugFilterKey', () => {
const entry = (column: string, value: string) => ({
column,
value,
label: value,
slug: value,
});
it('is stable whatever order the columns resolve in', () => {
expect(slugFilterKey([entry('platform', 'macos'), entry('status', 'done')])).toBe(
slugFilterKey([entry('status', 'done'), entry('platform', 'macos')])
);
});
it('changes when the selection moves', () => {
expect(slugFilterKey([entry('platform', 'macos')])).not.toBe(
slugFilterKey([entry('platform', 'linux')])
);
});
it('is empty when nothing is filtered', () => {
expect(slugFilterKey([])).toBe('');
});
});
@@ -0,0 +1,127 @@
import type { TableSelectColumn } from './search';
import type { SelectedOptions } from './searchMatch';
import { slugifySelectValue } from '@/lib/select';
/** One select column narrowed by the reader's content selection. */
export interface SlugFilterEntry {
/** Id of the select column being narrowed. */
column: string;
/** The option's opaque value, which the record matcher compares against. */
value: string;
/** The option's author-typed label, shown to the reader. */
label: string;
/** The active slug that narrowed the column, so the reader can clear it again. */
slug: string;
}
/**
* The `select` slug an option answers to.
*
* A table option's `value` is an opaque generated key, so it is the author-typed `label` that names
* it — the same wording a tab title or select button would carry, put through the same slugifier so
* a "macOS" column option and a "macOS" tab resolve to the one slug. Options with no label fall back
* to the raw value, mirroring how a cell renders one.
*/
export function getOptionSlug(option: { value: string; label: string }): string {
return slugifySelectValue(option.label || option.value);
}
/**
* Work out which option of each select column the reader's current selection points at.
*
* A table's select column declares its own option set, so a slug only filters a column that
* actually offers it: a reader who picked `macos` narrows a Platform column to macOS and leaves a
* Status column alone. Where several of a column's options are active at once, the most recently
* activated wins — the same rule tabs resolve with, so a table and the tabs beside it agree.
*/
export function resolveSlugFilter(
columns: TableSelectColumn[],
slugs: string[]
): SlugFilterEntry[] {
const entries: SlugFilterEntry[] = [];
for (const column of columns) {
let best: SlugFilterEntry | undefined;
let bestRank = Number.POSITIVE_INFINITY;
for (const option of column.options) {
const slug = getOptionSlug(option);
if (!slug) {
continue;
}
const rank = slugs.indexOf(slug);
if (rank >= 0 && rank < bestRank) {
bestRank = rank;
best = {
column: column.id,
value: option.value,
label: option.label || option.value,
slug,
};
}
}
if (best) {
entries.push(best);
}
}
return entries;
}
/** Stable identity for a filter, so it is only rebuilt when the selection actually moves. */
export function slugFilterKey(entries: SlugFilterEntry[]): string {
return entries
.map((entry) => `${entry.column}=${entry.value}`)
.sort()
.join(',');
}
/**
* Fold the columns the selection narrows into the reader's own filters.
*
* Kept pure, and given the previously narrowed columns rather than reading them from a ref, so it
* can be applied more than once without changing the answer — React may invoke a state updater
* twice, and an earlier version tracked those columns inside the updater itself, which silently
* undid a clear on the second pass.
*
* Only columns the selection narrowed last time are dropped; anything the reader filtered by hand
* is left exactly as it was.
*/
export function reconcileSelectedOptions(
previous: SelectedOptions,
previouslyNarrowed: readonly string[],
slugFilter: readonly SlugFilterEntry[]
): SelectedOptions {
if (previouslyNarrowed.length === 0 && slugFilter.length === 0) {
return previous;
}
const next = { ...previous };
for (const column of previouslyNarrowed) {
delete next[column];
}
for (const entry of slugFilter) {
next[entry.column] = new Set([entry.value]);
}
return next;
}
/**
* Narrow the filter to the columns the table is *still* showing.
*
* A reader can change any of these columns by hand, and that deliberately leaves the selection
* alone — the filter is this table's, for this visit, while the selection is site-wide and
* persists. So the notice has to stop speaking for a column that no longer matches, rather than
* describing a view the reader has since changed.
*/
export function getAppliedSlugFilter(
slugFilter: readonly SlugFilterEntry[],
selectedOptions: SelectedOptions
): SlugFilterEntry[] {
return slugFilter.filter((entry) => {
const values = selectedOptions[entry.column];
return values?.size === 1 && values.has(entry.value);
});
}
@@ -11,6 +11,8 @@ export const ar: TranslationLanguage = {
switch_to_system_theme: 'التبديل إلى سمة النظام',
search: 'بحث',
clear: 'مسح',
table_filtered_by_selection: 'يطابق الاختيار: ${1}',
table_clear_selection: 'مسح الاختيار',
tags: 'الوسوم',
search_back: 'العودة إلى نتائج البحث',
search_or_ask: 'اسأل أو ابحث',
@@ -11,6 +11,8 @@ export const bg: TranslationLanguage = {
switch_to_system_theme: 'Превключване към системната тема',
search: 'Търсене',
clear: 'Изчистване',
table_filtered_by_selection: 'Съответства на избора: ${1}',
table_clear_selection: 'Изчисти избора',
tags: 'Етикети',
search_back: 'Назад към резултатите от търсенето',
search_or_ask: 'Попитайте или търсете',
@@ -11,6 +11,8 @@ export const cs: TranslationLanguage = {
switch_to_system_theme: 'Přepnout na systémový motiv',
search: 'Hledat',
clear: 'Vymazat',
table_filtered_by_selection: 'Odpovídá výběru: ${1}',
table_clear_selection: 'Zrušit výběr',
tags: 'Štítky',
search_back: 'Zpět na výsledky hledání',
search_or_ask: 'Zeptat se nebo hledat',
@@ -11,6 +11,8 @@ export const da: TranslationLanguage = {
switch_to_system_theme: 'Skift til systemtema',
search: 'Søg',
clear: 'Ryd',
table_filtered_by_selection: 'Matcher valg: ${1}',
table_clear_selection: 'Ryd valg',
tags: 'Tags',
search_back: 'Tilbage til søgeresultater',
search_or_ask: 'Spørg eller søg',
@@ -11,6 +11,8 @@ export const de: TranslationLanguage = {
switch_to_system_theme: 'Zum Systemmodus wechseln',
search: 'Suche',
clear: 'Löschen',
table_filtered_by_selection: 'Entspricht Auswahl: ${1}',
table_clear_selection: 'Auswahl zurücksetzen',
tags: 'Tags',
search_back: 'Zurück zu den Suchergebnissen',
search_or_ask: 'Fragen oder Suchen',
@@ -11,6 +11,8 @@ export const el: TranslationLanguage = {
switch_to_system_theme: 'Αλλαγή στο θέμα συστήματος',
search: 'Αναζήτηση',
clear: 'Εκκαθάριση',
table_filtered_by_selection: 'Ταιριάζει με την επιλογή: ${1}',
table_clear_selection: 'Εκκαθάριση επιλογής',
tags: 'Ετικέτες',
search_back: 'Επιστροφή στα αποτελέσματα αναζήτησης',
search_or_ask: 'Ρωτήστε ή αναζητήστε',
@@ -9,6 +9,8 @@ export const en = {
switch_to_system_theme: 'Switch to system theme',
search: 'Search',
clear: 'Clear',
table_filtered_by_selection: 'Matches selection: ${1}',
table_clear_selection: 'Clear selection',
tags: 'Tags',
search_back: 'Back to search results',
search_or_ask: 'Ask or search',
@@ -11,6 +11,8 @@ export const es: TranslationLanguage = {
switch_to_system_theme: 'Cambiar a tema del sistema',
search: 'Buscar',
clear: 'Limpiar',
table_filtered_by_selection: 'Coincide con la selección: ${1}',
table_clear_selection: 'Borrar selección',
tags: 'Etiquetas',
search_back: 'Volver a los resultados de búsqueda',
search_or_ask: 'Preguntar o Buscar',
@@ -11,6 +11,8 @@ export const et: TranslationLanguage = {
switch_to_system_theme: 'Lülitu süsteemi teemale',
search: 'Otsi',
clear: 'Tühjenda',
table_filtered_by_selection: 'Vastab valikule: ${1}',
table_clear_selection: 'Tühjenda valik',
tags: 'Sildid',
search_back: 'Tagasi otsingutulemuste juurde',
search_or_ask: 'Küsi või otsi',
@@ -11,6 +11,8 @@ export const fi: TranslationLanguage = {
switch_to_system_theme: 'Vaihda järjestelmän teemaan',
search: 'Haku',
clear: 'Tyhjennä',
table_filtered_by_selection: 'Vastaa valintaa: ${1}',
table_clear_selection: 'Tyhjennä valinta',
tags: 'Tunnisteet',
search_back: 'Takaisin hakutuloksiin',
search_or_ask: 'Kysy tai hae',
@@ -11,6 +11,8 @@ export const fr: TranslationLanguage = {
switch_to_system_theme: 'Utiliser le thème du système',
search: 'Rechercher',
clear: 'Effacer',
table_filtered_by_selection: 'Correspond à la sélection : ${1}',
table_clear_selection: 'Effacer la sélection',
tags: 'Étiquettes',
search_back: 'Retour aux résultats de recherche',
search_or_ask: 'Rechercher',
@@ -11,6 +11,8 @@ export const he: TranslationLanguage = {
switch_to_system_theme: 'מעבר לערכת הנושא של המערכת',
search: 'חיפוש',
clear: 'ניקוי',
table_filtered_by_selection: 'תואם לבחירה: ${1}',
table_clear_selection: 'נקה בחירה',
tags: 'תגיות',
search_back: 'חזרה לתוצאות החיפוש',
search_or_ask: 'שאלה או חיפוש',
@@ -11,6 +11,8 @@ export const hi: TranslationLanguage = {
switch_to_system_theme: 'सिस्टम थीम पर जाएं',
search: 'खोजें',
clear: 'साफ करें',
table_filtered_by_selection: 'चयन से मेल खाता है: ${1}',
table_clear_selection: 'चयन साफ़ करें',
tags: 'टैग',
search_back: 'खोज परिणामों पर वापस जाएं',
search_or_ask: 'पूछें या खोजें',
@@ -11,6 +11,8 @@ export const hr: TranslationLanguage = {
switch_to_system_theme: 'Prebaci na temu sustava',
search: 'Pretraži',
clear: 'Očisti',
table_filtered_by_selection: 'Odgovara odabiru: ${1}',
table_clear_selection: 'Očisti odabir',
tags: 'Oznake',
search_back: 'Natrag na rezultate pretraživanja',
search_or_ask: 'Pitaj ili pretraži',
@@ -11,6 +11,8 @@ export const hu: TranslationLanguage = {
switch_to_system_theme: 'Váltás rendszer témára',
search: 'Keresés',
clear: 'Törlés',
table_filtered_by_selection: 'Megfelel a kijelölésnek: ${1}',
table_clear_selection: 'Kijelölés törlése',
tags: 'Címkék',
search_back: 'Vissza a keresési eredményekhez',
search_or_ask: 'Kérdezés vagy keresés',
@@ -11,6 +11,8 @@ export const id: TranslationLanguage = {
switch_to_system_theme: 'Beralih ke tema sistem',
search: 'Cari',
clear: 'Bersihkan',
table_filtered_by_selection: 'Sesuai pilihan: ${1}',
table_clear_selection: 'Hapus pilihan',
tags: 'Tag',
search_back: 'Kembali ke hasil pencarian',
search_or_ask: 'Tanya atau cari',
@@ -11,6 +11,8 @@ export const it: TranslationLanguage = {
switch_to_system_theme: 'Passa al tema di sistema',
search: 'Cerca',
clear: 'Cancella',
table_filtered_by_selection: 'Corrisponde alla selezione: ${1}',
table_clear_selection: 'Cancella selezione',
tags: 'Tag',
search_back: 'Torna ai risultati di ricerca',
search_or_ask: 'Chiedi o cerca',
@@ -11,6 +11,8 @@ export const ja: TranslationLanguage = {
switch_to_system_theme: 'システムのテーマに切り替え',
search: '検索',
clear: 'クリア',
table_filtered_by_selection: '選択に一致: ${1}',
table_clear_selection: '選択をクリア',
tags: 'タグ',
search_back: '検索結果に戻る',
search_or_ask: '質問または検索',
@@ -11,6 +11,8 @@ export const ko: TranslationLanguage = {
switch_to_system_theme: '시스템 테마로 전환',
search: '검색',
clear: '지우기',
table_filtered_by_selection: '선택과 일치: ${1}',
table_clear_selection: '선택 지우기',
tags: '태그',
search_back: '검색 결과로 돌아가기',
search_or_ask: '질문 또는 검색',
@@ -11,6 +11,8 @@ export const lt: TranslationLanguage = {
switch_to_system_theme: 'Perjungti į sistemos temą',
search: 'Ieškoti',
clear: 'Išvalyti',
table_filtered_by_selection: 'Atitinka pasirinkimą: ${1}',
table_clear_selection: 'Išvalyti pasirinkimą',
tags: 'Žymos',
search_back: 'Grįžti į paieškos rezultatus',
search_or_ask: 'Klausti arba ieškoti',
@@ -11,6 +11,8 @@ export const lv: TranslationLanguage = {
switch_to_system_theme: 'Pārslēgt uz sistēmas motīvu',
search: 'Meklēt',
clear: 'Notīrīt',
table_filtered_by_selection: 'Atbilst izvēlei: ${1}',
table_clear_selection: 'Notīrīt izvēli',
tags: 'Birkas',
search_back: 'Atpakaļ uz meklēšanas rezultātiem',
search_or_ask: 'Jautāt vai meklēt',
@@ -11,6 +11,8 @@ export const ms: TranslationLanguage = {
switch_to_system_theme: 'Tukar kepada tema sistem',
search: 'Cari',
clear: 'Kosongkan',
table_filtered_by_selection: 'Sepadan dengan pilihan: ${1}',
table_clear_selection: 'Kosongkan pilihan',
tags: 'Tag',
search_back: 'Kembali ke hasil carian',
search_or_ask: 'Tanya atau cari',
@@ -11,6 +11,8 @@ export const nl: TranslationLanguage = {
switch_to_system_theme: 'Schakel over naar systeemmodus',
search: 'Zoeken',
clear: 'Wissen',
table_filtered_by_selection: 'Komt overeen met selectie: ${1}',
table_clear_selection: 'Selectie wissen',
tags: 'Tags',
search_back: 'Terug naar zoekresultaten',
search_or_ask: 'Zoek of vraag',
@@ -11,6 +11,8 @@ export const no: TranslationLanguage = {
switch_to_system_theme: 'Bytt til systemtema',
search: 'Søk',
clear: 'Tøm',
table_filtered_by_selection: 'Samsvarer med valg: ${1}',
table_clear_selection: 'Fjern valg',
tags: 'Tagger',
search_back: 'Tilbake til søkeresultater',
search_or_ask: 'Spør eller søk',
@@ -11,6 +11,8 @@ export const pl: TranslationLanguage = {
switch_to_system_theme: 'Przełącz na motyw systemowy',
search: 'Szukaj',
clear: 'Wyczyść',
table_filtered_by_selection: 'Zgodne z wyborem: ${1}',
table_clear_selection: 'Wyczyść wybór',
tags: 'Tagi',
search_back: 'Wróć do wyników wyszukiwania',
search_or_ask: 'Zapytaj lub wyszukaj',
@@ -11,6 +11,8 @@ export const pt_br: TranslationLanguage = {
switch_to_system_theme: 'Mudar para configuração do sistema',
search: 'Buscar',
clear: 'Limpar',
table_filtered_by_selection: 'Corresponde à seleção: ${1}',
table_clear_selection: 'Limpar seleção',
tags: 'Tags',
search_back: 'Voltar aos resultados da busca',
search_or_ask: 'Perguntar ou buscar',
@@ -11,6 +11,8 @@ export const pt: TranslationLanguage = {
switch_to_system_theme: 'Mudar para o tema do sistema',
search: 'Pesquisar',
clear: 'Limpar',
table_filtered_by_selection: 'Corresponde à seleção: ${1}',
table_clear_selection: 'Limpar seleção',
tags: 'Etiquetas',
search_back: 'Voltar aos resultados da pesquisa',
search_or_ask: 'Perguntar ou pesquisar',
@@ -11,6 +11,8 @@ export const ro: TranslationLanguage = {
switch_to_system_theme: 'Comută la tema sistemului',
search: 'Caută',
clear: 'Șterge',
table_filtered_by_selection: 'Se potrivește cu selecția: ${1}',
table_clear_selection: 'Șterge selecția',
tags: 'Etichete',
search_back: 'Înapoi la rezultatele căutării',
search_or_ask: 'Întreabă sau caută',
@@ -11,6 +11,8 @@ export const ru: TranslationLanguage = {
switch_to_system_theme: 'Переключиться на системную тему',
search: 'Поиск',
clear: 'Очистить',
table_filtered_by_selection: 'Соответствует выбору: ${1}',
table_clear_selection: 'Очистить выбор',
tags: 'Теги',
search_back: 'Вернуться к результатам поиска',
search_or_ask: 'Найти или спросить',
@@ -11,6 +11,8 @@ export const sk: TranslationLanguage = {
switch_to_system_theme: 'Prepnúť na systémový motív',
search: 'Hľadať',
clear: 'Vymazať',
table_filtered_by_selection: 'Zodpovedá výberu: ${1}',
table_clear_selection: 'Zrušiť výber',
tags: 'Značky',
search_back: 'Späť na výsledky vyhľadávania',
search_or_ask: 'Opýtať sa alebo hľadať',
@@ -11,6 +11,8 @@ export const sl: TranslationLanguage = {
switch_to_system_theme: 'Preklopi na sistemsko temo',
search: 'Išči',
clear: 'Počisti',
table_filtered_by_selection: 'Ustreza izbiri: ${1}',
table_clear_selection: 'Počisti izbiro',
tags: 'Oznake',
search_back: 'Nazaj na rezultate iskanja',
search_or_ask: 'Vprašaj ali išči',
@@ -11,6 +11,8 @@ export const sv: TranslationLanguage = {
switch_to_system_theme: 'Byt till systemtema',
search: 'Sök',
clear: 'Rensa',
table_filtered_by_selection: 'Matchar val: ${1}',
table_clear_selection: 'Rensa val',
tags: 'Taggar',
search_back: 'Tillbaka till sökresultat',
search_or_ask: 'Fråga eller sök',
@@ -11,6 +11,8 @@ export const th: TranslationLanguage = {
switch_to_system_theme: 'เปลี่ยนเป็นธีมของระบบ',
search: 'ค้นหา',
clear: 'ล้าง',
table_filtered_by_selection: 'ตรงกับการเลือก: ${1}',
table_clear_selection: 'ล้างการเลือก',
tags: 'แท็ก',
search_back: 'กลับไปยังผลการค้นหา',
search_or_ask: 'ถามหรือค้นหา',
@@ -11,6 +11,8 @@ export const tr: TranslationLanguage = {
switch_to_system_theme: 'Sistem temasına geç',
search: 'Ara',
clear: 'Temizle',
table_filtered_by_selection: 'Seçimle eşleşiyor: ${1}',
table_clear_selection: 'Seçimi temizle',
tags: 'Etiketler',
search_back: 'Arama sonuçlarına geri dön',
search_or_ask: 'Sor veya ara',
@@ -11,6 +11,8 @@ export const uk: TranslationLanguage = {
switch_to_system_theme: 'Перемкнути на системну тему',
search: 'Пошук',
clear: 'Очистити',
table_filtered_by_selection: 'Відповідає вибору: ${1}',
table_clear_selection: 'Очистити вибір',
tags: 'Теги',
search_back: 'Назад до результатів пошуку',
search_or_ask: 'Запитати або шукати',
@@ -11,6 +11,8 @@ export const vi: TranslationLanguage = {
switch_to_system_theme: 'Chuyển sang giao diện hệ thống',
search: 'Tìm kiếm',
clear: 'Xóa',
table_filtered_by_selection: 'Khớp với lựa chọn: ${1}',
table_clear_selection: 'Xóa lựa chọn',
tags: 'Thẻ',
search_back: 'Quay lại kết quả tìm kiếm',
search_or_ask: 'Hỏi hoặc tìm kiếm',
@@ -11,6 +11,8 @@ export const yue: TranslationLanguage = {
switch_to_system_theme: '切換到系統主題',
search: '搜尋',
clear: '清除',
table_filtered_by_selection: '符合選擇:${1}',
table_clear_selection: '清除選擇',
tags: '標籤',
search_back: '返回搜尋結果',
search_or_ask: '發問或搜尋',
@@ -11,6 +11,8 @@ export const zh_tw: TranslationLanguage = {
switch_to_system_theme: '切換至系統主題',
search: '搜尋',
clear: '清除',
table_filtered_by_selection: '符合選擇:${1}',
table_clear_selection: '清除選擇',
tags: '標籤',
search_back: '返回搜尋結果',
search_or_ask: '詢問或搜尋',
@@ -11,6 +11,8 @@ export const zh: TranslationLanguage = {
switch_to_system_theme: '切换到系统主题',
search: '搜索',
clear: '清除',
table_filtered_by_selection: '符合选择:${1}',
table_clear_selection: '清除选择',
tags: '标签',
search_back: '返回搜索结果',
search_or_ask: '询问或搜索',