+ features={features}
ref={table}
data={machines(60)}
columns={excelColumns}
@@ -1329,6 +1358,7 @@ function MoveRowsTables() {
return (
+ features={features}
data={rows}
columns={machineColumns.slice(0, 4).map((column) => ({ ...column, filter: undefined }))}
caption="Machines by location"
@@ -1346,6 +1376,7 @@ function MoveRowsTables() {
}
/>
+ features={features}
data={files}
columns={fileColumns}
caption="Files"
@@ -1377,6 +1408,7 @@ export const ColumnGroups: Story = {
name: 'Managing column groups',
render: () => (
+ features={features}
data={machines(6)}
columns={[
{
@@ -1431,6 +1463,7 @@ function AdvancedFilterTable() {
const [tree, setTree] = useState(() => loadOver(50));
return (
extends Omit<
onPinnedColumnsChange?: (pinned: { start: string[]; end: string[] }) => void;
/** Shows an export button; the CSV has the filtered and sorted rows of all pages (or the selection) */
exportFilename?: string;
+ /** Separator of the CSV export, also of the export button – default: from the locale (";" where numbers have a
+ * decimal comma, e.g. de-DE, as a spreadsheet there expects it; "," otherwise) */
+ csvSeparator?: string;
/** Formats of the export button: CSV (default), Excel (.xlsx) or both */
exportFormats?: ('csv' | 'xlsx')[];
/** Groups the rows by the values of these columns (expandable group rows) */
@@ -417,7 +426,13 @@ export interface DataTableProps extends Omit<
fullscreenToggle?: boolean;
/** Fixed height (CSS length): the table scrolls inside, header and footer stay visible */
scrollHeight?: string;
- /** Only the visible rows are rendered (requires `scrollHeight`) */
+ /**
+ * Feature modules, imported on their own so apps only bundle what they use (fixed for the life of the table):
+ * `virtualRows` from `/virtual` for `virtual`, `dateInput` and `numberInput` for date and number filters and
+ * editors (without them the browser's inputs)
+ */
+ features?: DataTableFeature[];
+ /** Only the visible rows are rendered (requires `scrollHeight` and the feature `virtualRows`) */
virtual?: boolean;
/** Estimated row height in px for `virtual` */
rowHeight?: number;
@@ -445,7 +460,7 @@ export interface DataTableProps extends Omit<
classes?: RecipeClasses;
}
-type Features = typeof features;
+type Features = typeof tableFeatures;
const resolve = (updater: Updater, current: T) =>
typeof updater === 'function' ? (updater as (old: T) => T)(current) : updater;
@@ -491,6 +506,8 @@ const TEXT_CONTROLS = 'input, textarea, select, [contenteditable=""], [contented
/** Widths of the utility columns when columns have fixed widths */
const UTILITY_WIDTH = { drag: 40, select: 48, expand: 40, number: 56, actions: 88 } as const;
type Utility = keyof typeof UTILITY_WIDTH;
+/** Without the feature virtualRows: no virtualizer, all rows are rendered */
+const noVirtualizer = () => undefined;
/** Data table on TanStack Table: sorting (also by several columns), search, filter row or filter menus with match modes, pagination, selection, column menu (pin, move, hide, group), resizing, column groups, totals, row details, row actions, row reordering, editing (cell or row), virtual scrolling, full screen, CSV export and saved state */
export function DataTable({
@@ -567,6 +584,7 @@ export function DataTable({
pinnedColumns,
onPinnedColumnsChange,
exportFilename,
+ csvSeparator: csvSeparatorProp,
exportFormats,
groupBy,
onGroupByChange,
@@ -599,6 +617,7 @@ export function DataTable({
striped = false,
fullscreenToggle = false,
scrollHeight,
+ features,
virtual = false,
rowHeight = 41,
onLoadMore,
@@ -625,6 +644,8 @@ export function DataTable({
const t = translations.dataTable;
const id = useId();
const root = useRef(null);
+ // Read once: the virtualizer of `virtualRows` is a hook of this component
+ const [feature] = useState(() => Object.assign({}, ...(features ?? [])));
const labelFor = (row: TData, index: number) => rowLabelProp?.(row, index) ?? t.row(index);
const expandLabelFor = (row: TData, index: number) =>
expandLabelProp?.(row, index) ?? t.subRows(labelFor(row, index));
@@ -911,7 +932,7 @@ export function DataTable({
}, [columnDefs, locale]);
const table = useTable({
- features,
+ features: tableFeatures,
columns,
data,
getRowId: (row: TData, index: number) =>
@@ -995,7 +1016,7 @@ export function DataTable({
const grid = navigation === 'grid';
// A tree table with grid keys is a treegrid: rows with level and open state, left/right close and open them
const treeGrid = grid && !!getSubRows;
- const virtualOn = virtual && !!scrollHeight;
+ const virtualOn = virtual && !!scrollHeight && !!feature.useVirtualizer;
const paged = !!pageSize;
const styles = dataTable({ ...look, fullscreen, grid });
const styleFor = (columnId: string) => {
@@ -2085,7 +2106,7 @@ export function DataTable({
return true;
};
if (!reveal() && virtualOn) {
- virtualizer.scrollToIndex(
+ virtualizer?.scrollToIndex(
items.findIndex((item) => item.kind === 'row' && item.row.id === next.focus.rowId)
);
requestAnimationFrame(() => requestAnimationFrame(() => void reveal()));
@@ -2278,7 +2299,7 @@ export function DataTable({
});
const index = items.findIndex((item) => item.kind === 'row' && item.row.id === currentMatch.rowId);
if (virtualOn && index >= 0) {
- virtualizer.scrollToIndex(index, { align: 'center' });
+ virtualizer?.scrollToIndex(index, { align: 'center' });
requestAnimationFrame(() => requestAnimationFrame(show));
} else show();
});
@@ -2365,10 +2386,8 @@ export function DataTable({
: display(column, row.getValue(column.id), row.original)
)
);
- const csv = toCsv(
- [visible.map((column) => column.exportHeader ?? column.header), ...records],
- options.separator
- );
+ const separator = options.separator ?? csvSeparatorProp ?? csvSeparator(locale);
+ const csv = toCsv([visible.map((column) => column.exportHeader ?? column.header), ...records], separator);
return options.bom === false ? csv.slice(1) : csv;
};
const exportCsv = (options: boolean | DataTableCsvOptions = false) => {
@@ -2553,8 +2572,9 @@ export function DataTable({
// Scrolling: virtual rows and the end of the list
const scrollElement = useRef(null);
- // eslint-disable-next-line react-hooks/incompatible-library
- const virtualizer = useVirtualizer({
+ const useRows: (options: DataTableVirtualOptions) => DataTableVirtualizer | undefined =
+ feature.useVirtualizer ?? noVirtualizer;
+ const virtualizer = useRows({
count: virtualOn ? items.length : 0,
getScrollElement: () => scrollElement.current,
estimateSize: () => rowHeight,
@@ -2563,14 +2583,21 @@ export function DataTable({
// Rows of the first screen before the table is measured (no empty flash, SSR)
initialRect: { width: 0, height: Number.parseInt(scrollHeight ?? '', 10) || 400 },
});
- const virtualItems = virtualOn ? virtualizer.getVirtualItems() : [];
+ useEffect(() => {
+ if (virtual && !feature.useVirtualizer)
+ warnInDevelopment(
+ 'DataTable: `virtual` needs the feature virtualRows, so all rows are rendered – pass ' +
+ '`features={[virtualRows]}` with virtualRows from @wirecore/wirelayer-react/virtual'
+ );
+ }, [virtual, feature]);
+ const virtualItems = virtualOn ? (virtualizer?.getVirtualItems() ?? []) : [];
const shownItems = virtualOn
? virtualItems.map((item) => ({ ...items[item.index]!, index: item.index }))
: items.map((item, index) => ({ ...item, index }));
const paddingTop = virtualOn ? (virtualItems[0]?.start ?? 0) : 0;
- const paddingBottom = virtualOn ? virtualizer.getTotalSize() - (virtualItems.at(-1)?.end ?? 0) : 0;
+ const paddingBottom = virtualOn ? (virtualizer?.getTotalSize() ?? 0) - (virtualItems.at(-1)?.end ?? 0) : 0;
const measure = (element: HTMLTableRowElement | null) => {
- if (virtualOn && element) virtualizer.measureElement(element);
+ if (virtualOn && element) virtualizer?.measureElement(element);
};
const endReported = useRef(false);
useEffect(() => {
@@ -2627,7 +2654,7 @@ export function DataTable({
if (treeGrid && onTreeGridKey(event)) return;
gridKeydown(event.nativeEvent, tableElement.current, {
itemCount: items.length,
- scrollToItem: virtualOn ? (index) => virtualizer.scrollToIndex(index) : undefined,
+ scrollToItem: virtualOn ? (index) => virtualizer?.scrollToIndex(index) : undefined,
pageRows:
scrollElement.current && scrollHeight
? Math.max(1, Math.floor(scrollElement.current.clientHeight / rowHeight) - 1)
@@ -4013,5 +4040,9 @@ export function DataTable({
);
- return fullscreen ? createPortal(content, document.body) : {content} ;
+ return (
+
+ {fullscreen ? createPortal(content, document.body) : {content} }
+
+ );
}
diff --git a/packages/react/src/data-table/ValueInput.tsx b/packages/react/src/data-table/ValueInput.tsx
index fd25e6f..ad0a5c0 100644
--- a/packages/react/src/data-table/ValueInput.tsx
+++ b/packages/react/src/data-table/ValueInput.tsx
@@ -1,11 +1,11 @@
+import { useContext } from 'react';
import { Checkbox } from '../components/Checkbox/Checkbox.js';
-import { DatePicker } from '../components/DatePicker/DatePicker.js';
import { Input } from '../components/Input/Input.js';
import { MultiSelect } from '../components/MultiSelect/MultiSelect.js';
-import { NumberInput } from '../components/NumberInput/NumberInput.js';
import { Select } from '../components/Select/Select.js';
import { Textarea } from '../components/Textarea/Textarea.js';
import { useTranslations } from '../components/LocaleProvider/LocaleProvider.js';
+import { FeatureContext } from './features.js';
export interface ValueInputProps {
type: 'text' | 'textarea' | 'number' | 'select' | 'multiSelect' | 'date' | 'datetime' | 'boolean' | 'checkbox';
@@ -23,6 +23,8 @@ export interface ValueInputProps {
export function ValueInput({ type, value, onChange, label, options, any = false, invalid }: ValueInputProps) {
const translations = useTranslations();
const t = translations.dataTable;
+ // DatePicker and NumberInput come from the features dateInput and numberInput – without them the browser's inputs
+ const { dateInput: DatePicker, numberInput: NumberInput } = useContext(FeatureContext);
// Choices with values of their own keep them (objects, numbers); text choices stay text as before
const byValue = (options ?? []).some((option) => typeof option !== 'string');
const items: unknown[] = [
@@ -59,7 +61,7 @@ export function ValueInput({ type, value, onChange, label, options, any = false,
onChange={(event) => onChange(event.target.value)}
/>
);
- if (type === 'number')
+ if (type === 'number' && NumberInput)
return (
);
- if (type === 'date' || type === 'datetime')
+ if (type === 'number')
+ return (
+
+ onChange(target.value && Number.isFinite(Number(target.value)) ? Number(target.value) : null)
+ }
+ />
+ );
+ if ((type === 'date' || type === 'datetime') && DatePicker)
return (
);
+ if (type === 'date' || type === 'datetime')
+ return (
+ onChange(target.value || null)}
+ />
+ );
if (type === 'multiSelect')
return (
({});
diff --git a/packages/react/src/data-table/index.ts b/packages/react/src/data-table/index.ts
index 779468f..98c611d 100644
--- a/packages/react/src/data-table/index.ts
+++ b/packages/react/src/data-table/index.ts
@@ -1,5 +1,5 @@
-// DataTable on TanStack Table – requires @tanstack/react-table (and @tanstack/react-virtual for `virtual`) as optional
-// peer dependencies
+// DataTable on TanStack Table – requires @tanstack/react-table as an optional peer dependency (virtual rows: the feature
+// virtualRows from /virtual with @tanstack/react-virtual)
export { DataTable, type DataTableHandle, type DataTableProps } from './DataTable.js';
export type {
DataTableAdvancedFilterProps,
@@ -15,6 +15,7 @@ export type {
DataTableDataSource,
DataTableEditor,
DataTableEditorProps,
+ DataTableFeature,
DataTableFilterConstraint,
DataTableFilterInputProps,
DataTableFilterRule,
@@ -30,6 +31,8 @@ export type {
DataTableRowEdit,
DataTableRowReorder,
DataTableSort,
+ DataTableVirtualizer,
+ DataTableVirtualOptions,
DataTableXlsxOptions,
DataQuery,
DataRequest,
@@ -38,6 +41,8 @@ export type {
RowsRequest,
RowsResult,
} from './types.js';
+/** Feature modules for `features`: the DatePicker and NumberInput of date and number filters and editors */
+export { dateInput, numberInput } from './inputs.js';
/** Registers an own match mode for the filter menus of all tables (label, value input, test) */
export { defineMatchMode } from '@wirecore/wirelayer-recipes';
/** Moves a row in your data after a row-reorder event: `data = moveItem(data, from, to)` */
@@ -46,3 +51,6 @@ export { moveItem } from '@wirecore/wirelayer-recipes';
export { moveTreeRow } from '@wirecore/wirelayer-recipes';
/** A data source controller for own lists (abort, cache, debounced search) and the DataTable adapters */
export { createDataController, dataSourceToRows, rowsToDataSource } from '@wirecore/wirelayer-recipes';
+
+// Types of the recipes in the pivot props above
+export type { PivotValue } from '@wirecore/wirelayer-recipes';
diff --git a/packages/react/src/data-table/inputs.ts b/packages/react/src/data-table/inputs.ts
new file mode 100644
index 0000000..6718652
--- /dev/null
+++ b/packages/react/src/data-table/inputs.ts
@@ -0,0 +1,8 @@
+import { DatePicker } from '../components/DatePicker/DatePicker.js';
+import { NumberInput } from '../components/NumberInput/NumberInput.js';
+import type { DataTableFeature } from './types.js';
+
+/** DataTable feature: the DatePicker for `date` and `datetime` filters and editors (without: the browser's date input) */
+export const dateInput: DataTableFeature = { dateInput: DatePicker };
+/** DataTable feature: the NumberInput for `number` filters and editors (without: the browser's number input) */
+export const numberInput: DataTableFeature = { numberInput: NumberInput };
diff --git a/packages/react/src/data-table/types.ts b/packages/react/src/data-table/types.ts
index 38d68a0..3c772a8 100644
--- a/packages/react/src/data-table/types.ts
+++ b/packages/react/src/data-table/types.ts
@@ -1,4 +1,6 @@
-import type { ReactNode } from 'react';
+import type { ComponentType, ReactNode } from 'react';
+import type { DatePickerProps } from '../components/DatePicker/DatePicker.js';
+import type { NumberInputProps } from '../components/NumberInput/NumberInput.js';
export type {
DataTableAggregate,
DataTableFilterConstraint,
@@ -166,7 +168,7 @@ export interface DataTableAllSelection {
export interface DataTableCsvOptions {
/** Only the selected rows (default: the filtered and sorted rows of all pages) */
selectionOnly?: boolean;
- /** Separator, default ";" (opens in a German Excel) */
+ /** Separator – default: `csvSeparator` of the table, else from the locale (";" with a decimal comma, "," otherwise) */
separator?: string;
/** Byte order mark at the start, so Excel reads UTF-8 – default true */
bom?: boolean;
@@ -287,3 +289,34 @@ export interface DataTableRowAction {
row: TData;
rowId: string;
}
+
+/** What the table gives the virtualizer of `virtualRows` */
+export interface DataTableVirtualOptions {
+ count: number;
+ getScrollElement: () => HTMLElement | null;
+ estimateSize: (index: number) => number;
+ overscan: number;
+ getItemKey: (index: number) => string | number;
+ initialRect: { width: number; height: number };
+}
+
+/** The virtualizer of `virtualRows`: which rows to render and where they are */
+export interface DataTableVirtualizer {
+ getVirtualItems(): { index: number; start: number; end: number }[];
+ getTotalSize(): number;
+ measureElement(element: Element): void;
+ scrollToIndex(index: number, options?: { align?: 'start' | 'center' | 'end' | 'auto' }): void;
+}
+
+/**
+ * A feature module for `features`, imported on its own so apps only bundle what they use: `virtualRows` (from
+ * `@wirecore/wirelayer-react/data-table/virtual`, needs @tanstack/react-virtual), `dateInput` and `numberInput`
+ */
+export interface DataTableFeature {
+ /** Renders only the visible rows with `virtual` */
+ useVirtualizer?: (options: DataTableVirtualOptions) => DataTableVirtualizer;
+ /** Input of `date` and `datetime` filters and editors – without it the browser's date input */
+ dateInput?: ComponentType;
+ /** Input of `number` filters and editors – without it the browser's number input */
+ numberInput?: ComponentType;
+}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 33912be..da0e019 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -470,3 +470,60 @@ export {
type UploadState,
} from '@wirecore/wirelayer-recipes';
export * from './hooks/index.js';
+
+// Types of the recipes that appear in the props above – apps use them without depending on the recipes package
+export type {
+ AlertStatus,
+ AlertVariant,
+ AvatarColor,
+ AvatarShape,
+ AvatarSize,
+ AvatarVariant,
+ BadgeVariant,
+ BannerPosition,
+ BannerVariant,
+ ButtonColor,
+ ButtonShape,
+ ButtonSize,
+ ButtonVariant,
+ CalendarDayState,
+ CarouselBreakpoint,
+ ChipSize,
+ DataQuery,
+ DataSource,
+ DateInput,
+ DateLike,
+ DialogPosition,
+ DialogStatus,
+ DividerOrientation,
+ DrawerPlacement,
+ DurationValueType,
+ HighlightColor,
+ HotkeyRegistry,
+ IconButtonVariant,
+ ItemValueOf,
+ LayoutGap,
+ MaskTokens,
+ MegaMenuCollapse,
+ MeterColor,
+ OverlaySize,
+ PasswordRequirement,
+ RadioGroupOrientation,
+ RecipeClasses,
+ RecipeConfig,
+ RecurrenceScope,
+ ResponsiveColumns,
+ ResultStatus,
+ SankeyLinkInput,
+ SankeyNodeInput,
+ SchedulerDropReason,
+ SegmentedControlSize,
+ Size,
+ SkeletonShape,
+ SpeedDialDirection,
+ SpeedDialLayout,
+ StatusDotStatus,
+ StepStatus,
+ TreeCheckedStrategy,
+ VisualTone,
+} from '@wirecore/wirelayer-recipes';
diff --git a/packages/react/src/locales/de.ts b/packages/react/src/locales/de.ts
new file mode 100644
index 0000000..c127a38
--- /dev/null
+++ b/packages/react/src/locales/de.ts
@@ -0,0 +1,2 @@
+// German texts: registerLocale('de', de) at app start, or pass them to LocaleProvider (translations)
+export { de, de as default } from '@wirecore/wirelayer-recipes/locales/de';
diff --git a/packages/react/src/virtual/index.ts b/packages/react/src/virtual/index.ts
index f99816a..8c50082 100644
--- a/packages/react/src/virtual/index.ts
+++ b/packages/react/src/virtual/index.ts
@@ -2,3 +2,8 @@
export { VirtualSelect, type VirtualSelectProps } from './VirtualSelect.js';
export { VirtualList, type VirtualListHandle, type VirtualListProps } from './VirtualList.js';
export { VirtualTree, type VirtualTreeProps } from './VirtualTree.js';
+/** DataTable feature: virtual rows for `virtual` */
+export { virtualRows } from './virtual-rows.js';
+
+// Type of the recipes in the props above (item accessors: a key or a function)
+export type { ItemAccessor } from '@wirecore/wirelayer-recipes';
diff --git a/packages/react/src/virtual/virtual-rows.ts b/packages/react/src/virtual/virtual-rows.ts
new file mode 100644
index 0000000..ad44621
--- /dev/null
+++ b/packages/react/src/virtual/virtual-rows.ts
@@ -0,0 +1,5 @@
+import { useVirtualizer } from '@tanstack/react-virtual';
+import type { DataTableFeature } from '../data-table/types.js';
+
+/** DataTable feature: only the visible rows are rendered with `virtual` – `` */
+export const virtualRows: DataTableFeature = { useVirtualizer };
diff --git a/packages/recipes/src/blocks.ts b/packages/recipes/src/blocks.ts
index f34925a..f9778be 100644
--- a/packages/recipes/src/blocks.ts
+++ b/packages/recipes/src/blocks.ts
@@ -112,7 +112,7 @@ export function authFields(mode: AuthMode): AuthField[] {
}
export interface AuthValidationOptions {
- /** Messages of another language (default German) */
+ /** Messages of another language (default English) */
texts?: ValidationTexts;
/** Minimum length of a new password (sign up, default 8) */
passwordMinLength?: number;
diff --git a/packages/recipes/src/emoji.ts b/packages/recipes/src/emoji.ts
index 651358b..0d94fb1 100644
--- a/packages/recipes/src/emoji.ts
+++ b/packages/recipes/src/emoji.ts
@@ -1,9 +1,8 @@
-import { defaultLocale } from './i18n.js';
import { foldText } from './tree-data.js';
-// Emoji data for EmojiPicker: a selection of about 500 common emojis with German and English names and search words,
-// grouped by category. Skin tones are left out (the default yellow). Own data replaces or extends it (EmojiPicker
-// `emojis`); language modules add names and search words of further languages (`emojiPicker.emojis`).
+// Emoji data for EmojiPicker: a selection of about 500 common emojis with English names and search words, grouped by
+// category. Skin tones are left out (the default yellow). Own data replaces or extends it (EmojiPicker `emojis`);
+// language modules add names and search words of their language (`emojiPicker.emojis`, e.g. German in locales/de).
export type EmojiCategory =
'recent' | 'smileys' | 'people' | 'nature' | 'food' | 'travel' | 'activities' | 'objects' | 'symbols';
@@ -17,517 +16,9 @@ export interface EmojiData {
category: Exclude;
}
-// "emoji Name|Suchwörter" per line – compact to keep the bundle small
+// "emoji Name|search words" per line – compact to keep the bundle small
const raw: Record, string> = {
- smileys: `😀 Grinsendes Gesicht|lachen freude fröhlich
-😃 Grinsen mit großen Augen|lachen freude
-😄 Lachendes Gesicht|lachen freude glücklich
-😁 Strahlendes Gesicht|grinsen zähne
-😆 Lachen mit zugekniffenen Augen|lachen lustig
-😅 Lachen mit Schweißtropfen|erleichtert puh
-🤣 Vor Lachen auf dem Boden|lachen lol lustig
-😂 Tränen vor Lachen|lachen lustig weinen
-🙂 Leicht lächelndes Gesicht|lächeln
-🙃 Umgekehrtes Gesicht|ironie albern
-😉 Zwinkerndes Gesicht|zwinkern scherz
-😊 Lächeln mit lachenden Augen|freundlich erröten
-😇 Lächeln mit Heiligenschein|engel unschuldig
-🥰 Lächeln mit Herzen|verliebt liebe
-😍 Herzaugen|verliebt liebe toll
-🤩 Sternaugen|begeistert wow
-😘 Kuss zuwerfen|kuss liebe
-😋 Lecker|genießen essen zunge
-😛 Zunge herausstrecken|frech
-😜 Zwinkern mit Zunge|albern frech
-🤪 Verrücktes Gesicht|albern verrückt
-🤑 Geldgesicht|geld reich
-🤗 Umarmung|umarmen danke
-🤭 Hand vor dem Mund|kichern hoppla
-🤫 Pst|leise geheim ruhe
-🤔 Nachdenkliches Gesicht|denken überlegen hmm
-🤐 Reißverschlussmund|schweigen geheim
-🤨 Hochgezogene Augenbraue|skeptisch zweifel
-😐 Neutrales Gesicht|neutral egal
-😑 Ausdrucksloses Gesicht|genervt egal
-😶 Gesicht ohne Mund|sprachlos
-😏 Süffisantes Lächeln|grinsen schelmisch
-😒 Gelangweiltes Gesicht|unzufrieden genervt
-🙄 Augenrollen|genervt
-😬 Grimasse|peinlich angespannt
-😌 Erleichtertes Gesicht|zufrieden ruhig
-😔 Nachdenklich traurig|traurig
-😪 Müdes Gesicht|schläfrig
-😴 Schlafendes Gesicht|schlafen müde zzz
-😷 Gesicht mit Maske|krank maske
-🤒 Gesicht mit Thermometer|krank fieber
-🤢 Übelkeit|krank übel
-🤧 Niesendes Gesicht|schnupfen erkältung
-🥵 Heißes Gesicht|hitze schwitzen
-🥶 Kaltes Gesicht|kalt frieren
-😵 Benommenes Gesicht|schwindelig
-🤯 Explodierender Kopf|überwältigt wow
-🤠 Cowboy|hut
-🥳 Partygesicht|feiern party geburtstag
-😎 Sonnenbrille|cool lässig
-🤓 Strebergesicht|nerd brille
-🧐 Gesicht mit Monokel|prüfen genau
-😕 Verwirrtes Gesicht|unsicher
-😟 Besorgtes Gesicht|sorge
-🙁 Leicht trauriges Gesicht|traurig
-😮 Offener Mund|überrascht staunen
-😲 Erstauntes Gesicht|überrascht schock
-😳 Errötetes Gesicht|peinlich verlegen
-🥺 Flehendes Gesicht|bitte bettelnd
-😨 Ängstliches Gesicht|angst
-😰 Besorgt mit Schweiß|angst stress
-😢 Weinendes Gesicht|traurig träne
-😭 Laut weinend|traurig weinen
-😱 Schreiendes Gesicht|angst schock horror
-😖 Verwirrtes Gesicht|frustriert
-😞 Enttäuschtes Gesicht|enttäuscht traurig
-😓 Niedergeschlagen mit Schweiß|stress
-😩 Erschöpftes Gesicht|müde fertig
-😫 Müde|erschöpft
-🥱 Gähnendes Gesicht|gähnen müde langweilig
-😤 Triumphierendes Gesicht|schnauben stolz
-😡 Wütendes Gesicht|wut ärger
-😠 Verärgertes Gesicht|ärger böse
-🤬 Fluchendes Gesicht|schimpfen wut
-😈 Lachender Teufel|teufel frech
-💀 Totenkopf|tot schädel
-💩 Kothaufen|lustig
-🤡 Clown|clownsgesicht
-👻 Gespenst|geist halloween
-👽 Außerirdischer|alien ufo
-🤖 Roboter|bot maschine ki
-😺 Lachende Katze|katze
-😻 Katze mit Herzaugen|katze verliebt
-🙈 Nichts sehen|affe peinlich
-🙉 Nichts hören|affe
-🙊 Nichts sagen|affe geheim`,
- people: `👋 Winkende Hand|hallo tschüss winken
-🤚 Erhobene Hand|stopp melden
-✋ Hand|stopp halt fünf
-🖖 Vulkanischer Gruß|spock
-👌 OK-Zeichen|ok gut perfekt
-🤌 Zusammengelegte Finger|italienisch was
-✌️ Victory-Zeichen|sieg frieden
-🤞 Gekreuzte Finger|glück daumen drücken
-🤟 Ich liebe dich|liebe geste
-🤘 Teufelsgruß|rock metal
-🤙 Ruf mich an|telefon
-👈 Zeigefinger links|links zeigen
-👉 Zeigefinger rechts|rechts zeigen
-👆 Zeigefinger nach oben|oben
-👇 Zeigefinger nach unten|unten
-☝️ Zeigefinger|eins hinweis
-👍 Daumen hoch|gut ja zustimmung like
-👎 Daumen runter|schlecht nein ablehnung
-✊ Erhobene Faust|faust solidarität
-👊 Faust|faustschlag
-👏 Klatschende Hände|applaus bravo klatschen
-🙌 Erhobene Hände|feiern hurra
-👐 Offene Hände|umarmung
-🤲 Handflächen nach oben|bitten
-🤝 Handschlag|vereinbarung deal partner
-🙏 Gefaltete Hände|bitte danke beten
-✍️ Schreibende Hand|schreiben notiz
-💪 Bizeps|stark kraft muskel
-🧠 Gehirn|denken klug
-👀 Augen|schauen sehen
-👁️ Auge|sehen
-👄 Mund|lippen
-👶 Baby|kind säugling
-🧒 Kind|kind
-👦 Junge|kind
-👧 Mädchen|kind
-🧑 Person|mensch
-👨 Mann|mensch
-👩 Frau|mensch
-🧓 Ältere Person|alt senior
-👮 Polizist|polizei
-🕵️ Detektiv|spion ermittler
-👷 Bauarbeiter|baustelle helm
-🧑💻 Person am Computer|programmieren entwickler technik
-🧑🏫 Lehrkraft|lehrer schule
-🧑⚕️ Ärztin oder Arzt|medizin gesundheit
-🧑🍳 Koch|kochen küche
-🦸 Superheld|held
-🙋 Person mit erhobener Hand|melden frage hallo
-🤷 Schulterzucken|egal weiß nicht
-🤦 Hand vor die Stirn|facepalm oh nein
-🙇 Verbeugung|entschuldigung danke
-💁 Infoschalter-Person|info hilfe
-🏃 Laufende Person|laufen rennen sport
-🚶 Gehende Person|gehen spazieren
-💃 Tanzende Frau|tanzen party
-🕺 Tanzender Mann|tanzen party
-👨👩👧 Familie|eltern kind
-👥 Silhouetten|gruppe team leute
-🗣️ Sprechender Kopf|sprechen reden`,
- nature: `🐶 Hundegesicht|hund haustier
-🐱 Katzengesicht|katze haustier
-🐭 Mausgesicht|maus
-🐹 Hamster|haustier
-🐰 Hasengesicht|hase kaninchen ostern
-🦊 Fuchs|fuchs
-🐻 Bär|bär
-🐼 Panda|bär
-🐨 Koala|bär
-🐯 Tigergesicht|tiger
-🦁 Löwe|löwe
-🐮 Kuhgesicht|kuh
-🐷 Schweinegesicht|schwein
-🐸 Frosch|frosch
-🐵 Affengesicht|affe
-🐔 Huhn|huhn vogel
-🐧 Pinguin|vogel
-🐦 Vogel|vogel
-🦆 Ente|vogel
-🦅 Adler|vogel
-🦉 Eule|vogel nacht
-🐝 Biene|honig insekt
-🐛 Raupe|insekt
-🦋 Schmetterling|insekt
-🐌 Schnecke|langsam
-🐞 Marienkäfer|glück insekt
-🐢 Schildkröte|langsam
-🐍 Schlange|reptil
-🐙 Oktopus|krake meer
-🐠 Tropischer Fisch|fisch meer
-🐬 Delfin|meer
-🐳 Wal|meer
-🦈 Hai|meer
-🐊 Krokodil|reptil
-🦒 Giraffe|tier
-🐘 Elefant|tier
-🐴 Pferdegesicht|pferd
-🦄 Einhorn|fantasie
-🐾 Pfotenabdrücke|tier spuren
-💐 Blumenstrauß|blumen geschenk
-🌸 Kirschblüte|blume frühling
-🌹 Rose|blume liebe
-🌻 Sonnenblume|blume sommer
-🌷 Tulpe|blume frühling
-🌱 Keimling|pflanze wachsen
-🌲 Nadelbaum|baum wald
-🌳 Laubbaum|baum
-🌴 Palme|urlaub strand
-🌵 Kaktus|wüste pflanze
-🍀 Kleeblatt|glück
-🍁 Ahornblatt|herbst
-🍂 Herbstlaub|herbst blätter
-🌍 Weltkugel Europa und Afrika|erde welt global
-🌙 Mondsichel|mond nacht
-⭐ Stern|stern
-🌟 Leuchtender Stern|stern glanz
-⚡ Blitz|strom energie schnell
-🔥 Feuer|heiß flamme trend
-🌈 Regenbogen|farben
-☀️ Sonne|sonnig wetter
-⛅ Sonne hinter Wolke|wetter wolkig
-☁️ Wolke|wetter
-🌧️ Regenwolke|regen wetter
-❄️ Schneeflocke|schnee winter kalt
-💧 Tropfen|wasser
-🌊 Welle|meer wasser`,
- food: `🍏 Grüner Apfel|obst
-🍎 Roter Apfel|obst
-🍐 Birne|obst
-🍊 Mandarine|obst orange
-🍋 Zitrone|obst sauer
-🍌 Banane|obst
-🍉 Wassermelone|obst sommer
-🍇 Trauben|obst wein
-🍓 Erdbeere|obst
-🍒 Kirschen|obst
-🍑 Pfirsich|obst
-🥭 Mango|obst
-🍍 Ananas|obst
-🥥 Kokosnuss|obst
-🥝 Kiwi|obst
-🍅 Tomate|gemüse
-🥑 Avocado|gemüse
-🥦 Brokkoli|gemüse
-🥕 Karotte|möhre gemüse
-🌽 Maiskolben|mais gemüse
-🌶️ Peperoni|scharf
-🥔 Kartoffel|gemüse
-🍄 Pilz|pilz
-🥜 Erdnüsse|nüsse
-🍞 Brot|bäckerei
-🥐 Croissant|frühstück
-🥨 Brezel|bayern
-🧀 Käse|käse
-🥚 Ei|frühstück
-🍳 Spiegelei|frühstück braten
-🥞 Pfannkuchen|frühstück
-🥓 Speck|frühstück
-🍔 Hamburger|burger fastfood
-🍟 Pommes|fastfood
-🍕 Pizza|italienisch
-🌭 Hotdog|würstchen
-🥪 Sandwich|brot
-🌮 Taco|mexikanisch
-🥗 Salat|gesund
-🍝 Spaghetti|nudeln pasta
-🍜 Nudelsuppe|ramen
-🍣 Sushi|japanisch fisch
-🍦 Softeis|eis dessert
-🍩 Donut|süß
-🍪 Keks|süß
-🎂 Geburtstagskuchen|geburtstag torte feiern
-🍰 Kuchenstück|kuchen torte
-🧁 Cupcake|kuchen süß
-🍫 Schokolade|süß
-🍬 Bonbon|süß
-🍯 Honigtopf|honig
-☕ Heißgetränk|kaffee tee pause
-🍵 Tee|tee
-🥤 Becher mit Strohhalm|getränk
-🍺 Bier|getränk prost
-🍻 Anstoßende Biergläser|prost feiern
-🍷 Weinglas|wein getränk
-🥂 Sektgläser|anstoßen feiern prost
-🍾 Sektflasche|feiern
-🍽️ Teller mit Besteck|essen mahlzeit`,
- travel: `🚗 Auto|fahrzeug
-🚕 Taxi|fahrzeug
-🚌 Bus|fahrzeug verkehr
-🚑 Krankenwagen|notfall
-🚒 Feuerwehrauto|feuerwehr
-🚓 Polizeiauto|polizei
-🚚 Lieferwagen|lieferung lkw versand
-🚜 Traktor|landwirtschaft
-🚲 Fahrrad|rad
-🛴 Tretroller|roller
-🏍️ Motorrad|fahrzeug
-🚂 Lokomotive|zug bahn
-🚆 Zug|bahn reise
-🚇 U-Bahn|bahn
-✈️ Flugzeug|fliegen reise urlaub
-🛫 Abflug|fliegen reise
-🛬 Landung|fliegen ankunft
-🚀 Rakete|start launch schnell
-🛸 Ufo|außerirdisch
-🚁 Hubschrauber|fliegen
-⛵ Segelboot|boot segeln
-🚢 Schiff|boot reise
-⚓ Anker|schiff
-⛽ Zapfsäule|tanken benzin
-🚦 Ampel|verkehr
-🚧 Baustelle|arbeit sperrung
-🗺️ Weltkarte|karte reise
-🧭 Kompass|richtung navigation
-🏔️ Schneebedeckter Berg|berg
-🏖️ Strand mit Sonnenschirm|urlaub strand
-🏝️ Insel|urlaub
-🏠 Haus|zuhause heim
-🏡 Haus mit Garten|zuhause
-🏢 Bürogebäude|büro firma arbeit
-🏥 Krankenhaus|klinik
-🏦 Bank|geld
-🏨 Hotel|reise übernachtung
-🏫 Schule|bildung
-🏭 Fabrik|industrie
-🏰 Schloss|burg
-⛪ Kirche|religion
-🗼 Turm|tokio
-🗽 Freiheitsstatue|new york
-🎡 Riesenrad|kirmes
-🌃 Nacht mit Sternen|stadt nacht
-🌅 Sonnenaufgang|morgen
-⏰ Wecker|zeit wecker morgen
-⌛ Sanduhr|zeit warten
-⏱️ Stoppuhr|zeit messen
-🕐 Ein Uhr|uhr zeit`,
- activities: `⚽ Fußball|sport ball
-🏀 Basketball|sport ball
-🏈 Football|sport ball
-⚾ Baseball|sport ball
-🎾 Tennis|sport ball
-🏐 Volleyball|sport ball
-🏓 Tischtennis|sport
-🏸 Badminton|sport
-🥅 Tor|sport
-⛳ Golffahne|golf sport
-🎣 Angel|angeln fisch
-🎿 Ski|wintersport
-🏂 Snowboarder|wintersport
-🏊 Schwimmer|schwimmen sport
-🚴 Radfahrer|fahrrad sport
-🏆 Pokal|sieg gewinner preis
-🥇 Goldmedaille|erster sieg gewinner
-🥈 Silbermedaille|zweiter
-🥉 Bronzemedaille|dritter
-🏅 Sportmedaille|medaille preis
-🎖️ Orden|auszeichnung
-🎫 Eintrittskarte|ticket
-🎟️ Ticket|eintritt
-🎭 Theatermasken|theater kultur
-🎨 Farbpalette|kunst malen design
-🎬 Filmklappe|film kino
-🎤 Mikrofon|singen karaoke
-🎧 Kopfhörer|musik hören
-🎵 Musiknote|musik
-🎶 Musiknoten|musik
-🎹 Klaviatur|klavier musik
-🎸 Gitarre|musik
-🎮 Videospiel|spielen gaming controller
-🕹️ Joystick|spielen gaming
-🎲 Würfel|spiel glück
-🧩 Puzzleteil|puzzle lösung
-♟️ Schachfigur|schach strategie
-🎯 Volltreffer|ziel treffer
-🎳 Bowling|spiel
-🎉 Konfettibombe|feiern party glückwunsch
-🎊 Konfettiball|feiern party
-🎈 Luftballon|party geburtstag
-🎁 Geschenk|geburtstag überraschung
-🎄 Weihnachtsbaum|weihnachten
-🎃 Kürbislaterne|halloween
-✨ Funkeln|glanz neu magie`,
- objects: `⌚ Armbanduhr|uhr zeit
-📱 Mobiltelefon|handy smartphone
-💻 Laptop|computer rechner arbeit
-⌨️ Tastatur|computer tippen
-🖥️ Bildschirm|computer monitor
-🖨️ Drucker|drucken
-🖱️ Computermaus|maus klicken
-💾 Diskette|speichern
-💿 CD|daten musik
-📷 Kamera|foto
-📹 Videokamera|video
-📞 Telefonhörer|telefon anrufen
-☎️ Telefon|anrufen
-📺 Fernseher|tv
-📻 Radio|musik
-🔋 Batterie|akku energie
-🔌 Stecker|strom
-💡 Glühbirne|idee licht tipp
-🔦 Taschenlampe|licht
-🕯️ Kerze|licht
-🗑️ Papierkorb|löschen müll
-💰 Geldsack|geld reich
-💳 Kreditkarte|bezahlen geld
-💶 Euroschein|geld euro
-🧾 Beleg|quittung rechnung
-✉️ Briefumschlag|brief post mail
-📧 E-Mail|mail nachricht
-📨 Eingehender Umschlag|mail empfangen
-📦 Paket|versand lieferung
-📮 Briefkasten|post
-📝 Notiz|schreiben memo
-✏️ Bleistift|schreiben bearbeiten
-🖊️ Kugelschreiber|schreiben
-📁 Ordner|datei ablage
-📂 Geöffneter Ordner|datei
-📄 Dokument|datei seite
-📊 Balkendiagramm|statistik daten chart
-📈 Steigender Trend|wachstum statistik
-📉 Fallender Trend|rückgang statistik
-📅 Kalender|datum termin
-📆 Abreißkalender|datum termin
-📌 Pinnnadel|merken wichtig
-📍 Stecknadel|ort standort
-📎 Büroklammer|anhang
-📏 Lineal|messen
-✂️ Schere|schneiden
-🔒 Geschlossenes Schloss|sicher gesperrt privat
-🔓 Offenes Schloss|entsperrt
-🔑 Schlüssel|passwort zugang
-🔨 Hammer|werkzeug bauen
-🛠️ Werkzeuge|reparieren einstellungen
-⚙️ Zahnrad|einstellungen technik
-🧰 Werkzeugkasten|werkzeug
-🧲 Magnet|anziehen
-🧪 Reagenzglas|labor test chemie
-🔬 Mikroskop|wissenschaft forschung
-🔭 Teleskop|astronomie
-💊 Tablette|medizin
-🩺 Stethoskop|arzt medizin
-🛒 Einkaufswagen|einkaufen shop
-🎒 Rucksack|schule
-👓 Brille|lesen
-📚 Bücher|lesen lernen bibliothek
-📖 Offenes Buch|lesen
-🔍 Lupe|suchen suche
-🔔 Glocke|benachrichtigung
-📣 Megafon|ankündigung
-🏷️ Etikett|tag label preis
-🔗 Link|verknüpfung kette`,
- symbols: `❤️ Rotes Herz|liebe herz
-🧡 Oranges Herz|herz
-💛 Gelbes Herz|herz
-💚 Grünes Herz|herz
-💙 Blaues Herz|herz
-💜 Lila Herz|herz
-🖤 Schwarzes Herz|herz
-🤍 Weißes Herz|herz
-💔 Gebrochenes Herz|trauer liebe
-💕 Zwei Herzen|liebe
-💯 Hundert Punkte|perfekt voll
-💢 Ärger-Symbol|wut
-💥 Zusammenstoß|knall boom
-💫 Schwindelig|sterne
-💬 Sprechblase|nachricht kommentar chat
-💭 Gedankenblase|denken
-💤 Schlafen|zzz müde
-✅ Weißes Häkchen auf Grün|erledigt ja fertig ok
-☑️ Abgehaktes Kästchen|erledigt
-✔️ Häkchen|ok richtig
-❌ Kreuz|nein falsch fehler löschen
-❎ Kreuz-Schaltfläche|nein
-➕ Plus|hinzufügen mehr
-➖ Minus|weniger entfernen
-❓ Fragezeichen|frage hilfe
-❗ Ausrufezeichen|wichtig achtung
-⚠️ Warnung|achtung vorsicht
-⛔ Zutritt verboten|stopp verbot
-🚫 Verboten|nein verbot
-♻️ Recycling|umwelt
-✳️ Achtzackiger Stern|stern
-🆕 Neu|neu
-🆗 OK|ok
-🆘 SOS|hilfe notfall
-🔴 Roter Kreis|rot punkt
-🟠 Oranger Kreis|orange punkt
-🟡 Gelber Kreis|gelb punkt
-🟢 Grüner Kreis|grün punkt
-🔵 Blauer Kreis|blau punkt
-🟣 Lila Kreis|lila punkt
-⚫ Schwarzer Kreis|schwarz punkt
-⚪ Weißer Kreis|weiß punkt
-🔺 Rotes Dreieck|nach oben
-🔻 Rotes Dreieck nach unten|nach unten
-⬆️ Pfeil nach oben|oben hoch
-⬇️ Pfeil nach unten|unten runter
-⬅️ Pfeil nach links|links zurück
-➡️ Pfeil nach rechts|rechts weiter
-🔄 Pfeile gegen den Uhrzeigersinn|aktualisieren neu laden
-🔁 Wiederholen|schleife
-▶️ Wiedergabe|abspielen play
-⏸️ Pause|pause
-⏹️ Stopp|stopp
-🔇 Stumm|ton aus
-🔊 Lautsprecher laut|ton an
-©️ Copyright|recht
-®️ Eingetragene Marke|marke
-™️ Markenzeichen|marke
-#️⃣ Raute|hashtag nummer
-🔢 Zahlen|nummer
-ℹ️ Information|info hinweis
-🏁 Zielflagge|ziel rennen fertig
-🚩 Dreiecksflagge|markierung warnung
-🏳️ Weiße Flagge|aufgeben
-🇩🇪 Flagge Deutschland|deutschland
-🇦🇹 Flagge Österreich|österreich
-🇨🇭 Flagge Schweiz|schweiz
-🇪🇺 Flagge Europäische Union|eu europa`,
-};
-
-// The same emojis in English ("emoji Name|search words" per line)
-const rawEnglish = `😀 Grinning face|smile happy joy
+ smileys: `😀 Grinning face|smile happy joy
😃 Grinning face with big eyes|smile happy
😄 Grinning face with smiling eyes|laugh happy
😁 Beaming face|grin teeth
@@ -612,8 +103,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
😻 Cat with heart eyes|cat love
🙈 See-no-evil monkey|monkey
🙉 Hear-no-evil monkey|monkey
-🙊 Speak-no-evil monkey|monkey
-👋 Waving hand|wave hello bye
+🙊 Speak-no-evil monkey|monkey`,
+ people: `👋 Waving hand|wave hello bye
🤚 Raised back of hand|hand
✋ Raised hand|hand stop high five
🖖 Vulcan salute|spock
@@ -672,8 +163,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
🕺 Man dancing|dance
👨👩👧 Family|parents child
👥 Busts in silhouette|people users team
-🗣️ Speaking head|speak talk
-🐶 Dog face|dog puppy
+🗣️ Speaking head|speak talk`,
+ nature: `🐶 Dog face|dog puppy
🐱 Cat face|cat kitten
🐭 Mouse face|mouse
🐹 Hamster|pet
@@ -738,8 +229,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
🌧️ Cloud with rain|rain weather
❄️ Snowflake|snow cold winter
💧 Droplet|water drop
-🌊 Water wave|wave sea ocean
-🍏 Green apple|fruit
+🌊 Water wave|wave sea ocean`,
+ food: `🍏 Green apple|fruit
🍎 Red apple|fruit
🍐 Pear|fruit
🍊 Tangerine|orange fruit
@@ -798,8 +289,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
🍷 Wine glass|wine
🥂 Clinking glasses|cheers champagne toast
🍾 Bottle with popping cork|champagne celebrate
-🍽️ Fork and knife with plate|dinner meal
-🚗 Automobile|car
+🍽️ Fork and knife with plate|dinner meal`,
+ travel: `🚗 Automobile|car
🚕 Taxi|cab
🚌 Bus|public transport
🚑 Ambulance|emergency
@@ -848,8 +339,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
⏰ Alarm clock|alarm time
⌛ Hourglass done|time wait
⏱️ Stopwatch|timer time
-🕐 One o’clock|time clock
-⚽ Soccer ball|football sport
+🕐 One o’clock|time clock`,
+ activities: `⚽ Soccer ball|football sport
🏀 Basketball|sport
🏈 American football|sport
⚾ Baseball|sport
@@ -894,8 +385,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
🎁 Wrapped gift|present gift
🎄 Christmas tree|christmas
🎃 Jack-o-lantern|halloween pumpkin
-✨ Sparkles|shine magic new
-⌚ Watch|time
+✨ Sparkles|shine magic new`,
+ objects: `⌚ Watch|time
📱 Mobile phone|smartphone
💻 Laptop|computer
⌨️ Keyboard|computer type
@@ -963,8 +454,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
🔔 Bell|notification
📣 Megaphone|announcement
🏷️ Label|tag
-🔗 Link|chain url
-❤️ Red heart|love
+🔗 Link|chain url`,
+ symbols: `❤️ Red heart|love
🧡 Orange heart|love
💛 Yellow heart|love
💚 Green heart|love
@@ -1031,7 +522,8 @@ const rawEnglish = `😀 Grinning face|smile happy joy
🇩🇪 Flag Germany|germany
🇦🇹 Flag Austria|austria
🇨🇭 Flag Switzerland|switzerland
-🇪🇺 Flag European Union|eu europe`;
+🇪🇺 Flag European Union|eu europe`,
+};
/** Order of the categories (recently used first) */
export const emojiCategories: EmojiCategory[] = [
@@ -1048,7 +540,7 @@ export const emojiCategories: EmojiCategory[] = [
let parsed: EmojiData[] | undefined;
-/** The built-in emojis (parsed once) */
+/** The built-in emojis with their English names (parsed once) */
export function defaultEmojis(): EmojiData[] {
parsed ??= (Object.entries(raw) as [EmojiData['category'], string][]).flatMap(([category, lines]) =>
lines.split('\n').map((line) => {
@@ -1072,29 +564,24 @@ function parseEmojiTexts(lines: string) {
return texts;
}
-let english: Map | undefined;
-const byLocale = new Map();
+const byTexts = new Map();
/**
- * The built-in emojis for a locale: German names in German, English ones otherwise. `texts` – lines "😀 Name|search
- * words", e.g. `emojiPicker.emojis` of a language module – give names and search words of another language. The
- * English name and words always find an emoji as well (and in German the German ones).
+ * The built-in emojis in a language: `texts` – lines "😀 Name|search words", e.g. `emojiPicker.emojis` of a language
+ * module – give names and search words; emojis without a line keep their English name. The English name and words
+ * always find an emoji as well.
*/
-export function localeEmojis(locale = defaultLocale, texts = ''): EmojiData[] {
- const german = locale.split('-')[0]!.toLowerCase() === 'de';
- const key = `${german ? 'de' : 'other'}\n${texts}`;
- let result = byLocale.get(key);
+export function localeEmojis(texts = ''): EmojiData[] {
+ let result = byTexts.get(texts);
if (result) return result;
- english ??= parseEmojiTexts(rawEnglish);
const own = parseEmojiTexts(texts);
result = defaultEmojis().map((entry) => {
- const en = english!.get(entry.emoji);
- const local = own.get(entry.emoji) ?? (german ? { name: entry.name, keywords: entry.keywords ?? [] } : en);
+ const local = own.get(entry.emoji);
if (!local) return entry;
- const extra = local === en || !en ? [] : [...en.name.toLowerCase().split(' '), ...en.keywords];
- return { ...entry, name: local.name, keywords: [...new Set([...local.keywords, ...extra])] };
+ const english = [...entry.name.toLowerCase().split(' '), ...(entry.keywords ?? [])];
+ return { ...entry, name: local.name, keywords: [...new Set([...local.keywords, ...english])] };
});
- byLocale.set(key, result);
+ byTexts.set(texts, result);
return result;
}
diff --git a/packages/recipes/src/i18n.ts b/packages/recipes/src/i18n.ts
index 6c20ced..20cf0ac 100644
--- a/packages/recipes/src/i18n.ts
+++ b/packages/recipes/src/i18n.ts
@@ -1,1312 +1,12 @@
// UI texts of the components – shared by @wirecore/wirelayer-vue and @wirecore/wirelayer-react so both read the same to assistive
-// tech. German is the default; LocaleProvider switches the language (see resolveLocale). German and English are built
-// in; further languages are separate modules (./locales/.ts, `@wirecore/wirelayer-recipes/locales/`) that an app
+// tech. English is the default and built in; LocaleProvider switches the language (see resolveLocale). German and the
+// further languages are separate modules (./locales/.ts, `@wirecore/wirelayer-recipes/locales/`) that an app
// registers with registerLocale, so they do not add to the default bundle.
-type CalendarView = 'day' | 'month' | 'year';
-
-const next: Record = {
- day: 'Nächster Monat',
- month: 'Nächstes Jahr',
- year: 'Nächstes Jahrzehnt',
-};
-const previous: Record = {
- day: 'Vorheriger Monat',
- month: 'Vorheriges Jahr',
- year: 'Vorheriges Jahrzehnt',
-};
-const viewNames: Record = { day: 'Tage', month: 'Monate', year: 'Jahre' };
+// The German texts define the shape of every language (type only – they are not part of this bundle)
+import type { de } from './locales/de.js';
-const de = {
- common: {
- close: 'Schließen',
- cancel: 'Abbrechen',
- confirm: 'Bestätigen',
- placeholder: 'Bitte wählen',
- loading: 'Wird geladen',
- empty: 'Keine Einträge',
- noResults: 'Keine Treffer',
- selectedCount: (count: number) => `${count} ausgewählt`,
- },
- toast: { label: 'Benachrichtigungen' },
- menu: { title: 'Aktionen', search: 'Menü durchsuchen' },
- menubar: { menu: 'Menü' },
- dialog: { maximize: 'Maximieren', restore: 'Verkleinern' },
- /** confirm() and prompt() */
- confirm: {
- typeToConfirm: (text: string) => `Zur Bestätigung „${text}“ eingeben`,
- mismatch: 'Die Eingabe stimmt nicht überein',
- },
- breadcrumb: { label: 'Pfad', more: 'Ausgeblendete Ebenen' },
- siteHeader: { navigation: 'Hauptnavigation', menu: 'Menü' },
- modeToggle: { light: 'Helles Design aktivieren', dark: 'Dunkles Design aktivieren' },
- pagination: {
- rootLabel: 'Seitennavigation',
- prevTriggerLabel: 'Vorherige Seite',
- nextTriggerLabel: 'Nächste Seite',
- itemLabel: (details: { page: number; totalPages: number }) => `Seite ${details.page} von ${details.totalPages}`,
- summary: (range: { start: number; end: number; count: number }) =>
- `${range.start}–${range.end} von ${range.count}`,
- firstTriggerLabel: 'Erste Seite',
- lastTriggerLabel: 'Letzte Seite',
- /** Compact variant: the current page between the arrows */
- compact: (page: number, totalPages: number) => `Seite ${page} von ${totalPages}`,
- pageSize: 'Pro Seite',
- jumper: 'Gehe zu Seite',
- },
- saveStatus: {
- idle: '',
- saving: 'Wird gespeichert …',
- saved: 'Gespeichert',
- error: 'Speichern fehlgeschlagen',
- },
- slider: { thumbs: ['Minimum', 'Maximum'] },
- numberInput: { incrementLabel: 'Erhöhen', decrementLabel: 'Verringern' },
- passwordInput: {
- visibilityTrigger: (visible: boolean): string => (visible ? 'Passwort verbergen' : 'Passwort anzeigen'),
- },
- combobox: { triggerLabel: 'Vorschläge anzeigen', clearTriggerLabel: 'Eingabe löschen', search: 'Suchen …' },
- datePicker: {
- dayCell: (state: { valueText: string; selected: boolean; unavailable: boolean }) =>
- state.unavailable
- ? `Nicht verfügbar: ${state.valueText}`
- : state.selected
- ? `Ausgewählt: ${state.valueText}`
- : state.valueText,
- nextTrigger: (view: CalendarView) => next[view],
- prevTrigger: (view: CalendarView) => previous[view],
- viewTrigger: (_view: CalendarView, nextView: CalendarView | undefined) =>
- nextView ? `${viewNames[nextView]} anzeigen` : 'Ansicht wechseln',
- monthSelect: 'Monat wählen',
- yearSelect: 'Jahr wählen',
- clearTrigger: 'Datum löschen',
- trigger: (open: boolean): string => (open ? 'Kalender schließen' : 'Kalender öffnen'),
- content: 'Kalender',
- placeholder: () => ({ year: 'JJJJ', month: 'MM', day: 'TT' }),
- /** Name of the week column (its visible header is weekColumnShort) */
- weekColumnHeader: 'Kalenderwoche',
- weekColumnShort: 'KW',
- weekNumberCell: (week: number) => `Kalenderwoche ${week}`,
- presetTrigger: (value: string[]) => value.join(' – '),
- },
- /** Calendar extras: button bar, several chosen days, time zone */
- calendar: {
- today: 'Heute',
- clear: 'Löschen',
- /** Field of a DatePicker with selectionMode multiple */
- noDates: 'Kein Datum gewählt',
- datesSelected: { one: '{count} Tag ausgewählt', other: '{count} Tage ausgewählt' } as PluralForms,
- timeZone: (name: string) => `Zeitzone: ${name}`,
- },
- quarterPicker: {
- /** Text of a quarter in the field and on its button */
- quarter: (quarter: number, year: number) => `Q${quarter} ${year}`,
- /** Name of a quarter button: its text and the months */
- quarterButton: (quarter: number, year: number, months: string) => `Q${quarter} ${year}, ${months}`,
- trigger: 'Quartal wählen',
- content: 'Quartale',
- prevYear: 'Vorheriges Jahr',
- nextYear: 'Nächstes Jahr',
- clearTrigger: 'Quartal löschen',
- placeholder: 'Q1 2026',
- },
- durationInput: {
- clearTrigger: 'Dauer löschen',
- days: 'Tage',
- hours: 'Stunden',
- minutes: 'Minuten',
- seconds: 'Sekunden',
- },
- timeGrid: {
- label: 'Freie Termine',
- empty: 'Keine freien Termine',
- loading: 'Termine werden geladen',
- available: { one: '{count} freier Termin', other: '{count} freie Termine' } as PluralForms,
- },
- miniCalendar: {
- label: 'Tage',
- previous: 'Vorherige Tage',
- next: 'Nächste Tage',
- },
- /** DateInput: placeholders of the parts and names of their spin buttons */
- dateInput: {
- placeholder: () => ({
- day: 'TT',
- month: 'MM',
- year: 'JJJJ',
- hour: 'hh',
- minute: 'mm',
- second: 'ss',
- dayPeriod: '--',
- era: '--',
- }),
- },
- splitButton: { more: 'Weitere Aktionen' },
- confirmPopup: { confirm: 'Ja', cancel: 'Abbrechen' },
- passwordStrength: {
- levels: ['Zu kurz', 'Schwach', 'Mittel', 'Gut', 'Stark'],
- label: (level: string) => `Passwortstärke: ${level}`,
- },
- /** Field and text controls (Input, Textarea, PasswordInput, NumberInput …) */
- field: { optional: 'optional', clear: 'Eingabe löschen' },
- textarea: {
- /** Visible counter */
- counter: (length: number, max: number) => `${length} / ${max}`,
- /** Announced after typing pauses */
- remaining: (remaining: number, max: number) =>
- remaining <= 0 ? `Höchstlänge von ${max} Zeichen erreicht` : `Noch ${remaining} von ${max} Zeichen`,
- },
- formErrorSummary: {
- title: (count: number) =>
- count === 1 ? 'Bitte korrigieren Sie 1 Angabe' : `Bitte korrigieren Sie ${count} Angaben`,
- },
- passwordRequirements: { label: 'Anforderungen an das Passwort', met: 'erfüllt', unmet: 'nicht erfüllt' },
- dataView: {
- layout: 'Ansicht',
- list: 'Liste',
- grid: 'Raster',
- loadMore: 'Mehr laden',
- /** Names the list of loading placeholders */
- loading: 'Einträge werden geladen',
- shown: (count: number, total: number) => `${count} von ${total} angezeigt`,
- },
- orderList: {
- top: 'Ganz nach oben',
- up: 'Nach oben',
- down: 'Nach unten',
- bottom: 'Ganz nach unten',
- moved: (label: string, position: number, count: number) => `${label}: Position ${position} von ${count}`,
- },
- pickList: {
- source: 'Verfügbar',
- target: 'Ausgewählt',
- add: 'Ausgewählte hinzufügen',
- addAll: 'Alle hinzufügen',
- remove: 'Ausgewählte entfernen',
- removeAll: 'Alle entfernen',
- moved: (count: number, target: string) =>
- `${count === 1 ? '1 Eintrag' : `${count} Einträge`} nach „${target}“ verschoben`,
- /** Search field of one list */
- filter: (list: string) => `${list} durchsuchen`,
- /** Drag and drop: where the dragged items go */
- dropHint: (list: string) => `Hierher ziehen: ${list}`,
- },
- appShell: {
- skipLink: 'Zum Inhalt springen',
- openNavigation: 'Navigation öffnen',
- navigation: 'Navigation',
- collapseNavigation: 'Navigation einklappen',
- expandNavigation: 'Navigation ausklappen',
- resizeNavigation: 'Breite der Navigation ändern',
- resizeAside: 'Breite der Seitenleiste ändern',
- },
- commandPalette: {
- label: 'Befehle',
- placeholder: 'Suchen oder Befehl eingeben …',
- empty: 'Nichts gefunden',
- navigate: 'auswählen',
- run: 'ausführen',
- close: 'schließen',
- },
- /** Option lists: Select, MultiSelect, Combobox, Listbox, TagsInput suggestions */
- select: {
- clear: 'Auswahl löschen',
- filter: 'Optionen filtern',
- selectAll: 'Alle auswählen',
- removeItem: (label: string) => `${label} entfernen`,
- removed: (label: string) => `${label} entfernt`,
- maxReached: (max: number) => `Höchstens ${max} auswählbar`,
- suggestions: 'Vorschläge',
- },
- /** Standalone Chip: name of the remove button */
- chip: { remove: (label?: string): string => (label ? `${label} entfernen` : 'Entfernen') },
- tagsInput: {
- clearTriggerLabel: 'Alle entfernen',
- deleteTagTriggerLabel: (value: string) => `${value} entfernen`,
- tagAdded: (value: string) => `${value} hinzugefügt`,
- tagsPasted: (values: string[]) => `${values.length} Einträge eingefügt`,
- tagEdited: (value: string) => `${value} wird bearbeitet`,
- tagUpdated: (value: string) => `${value} geändert`,
- tagDeleted: (value: string) => `${value} entfernt`,
- tagSelected: (value: string) => `${value} ausgewählt, Rücktaste zum Entfernen`,
- },
- navigationMenu: { rootLabel: 'Hauptnavigation' },
- megaMenu: {
- rootLabel: 'Hauptnavigation',
- /** Menu button and drawer title on small screens */
- menu: 'Menü',
- /** Chevron button next to an entry that is a link itself */
- submenu: (label: string) => `${label}: Untermenü`,
- showAll: 'Alle anzeigen',
- showAllOf: (title: string) => `Alle anzeigen: ${title}`,
- /** Link of the featured area */
- featuredLink: 'Mehr erfahren',
- /** In the drawer: link to the page of an entry that also has a panel */
- overview: (label: string) => `${label} – Übersicht`,
- },
- pageHeader: { back: 'Zurück' },
- bottomNavigation: { label: 'Navigation' },
- banner: { dismiss: 'Hinweis schließen' },
- notificationCenter: {
- label: 'Benachrichtigungen',
- markAllRead: 'Alle als gelesen markieren',
- markRead: 'Als gelesen markieren',
- unread: 'Ungelesen',
- unreadCount: (count: number | string) => `${count} ungelesen`,
- empty: 'Keine Benachrichtigungen',
- loadMore: 'Mehr laden',
- older: 'Älter',
- /** Polite announcement of new notifications */
- announce: (count: number, title: string) =>
- count === 1 ? `Neue Benachrichtigung: ${title}` : `${count} neue Benachrichtigungen`,
- },
- result: {
- success: 'Erledigt',
- info: 'Hinweis',
- warn: 'Achtung',
- error: 'Etwas ist schiefgelaufen',
- '403': 'Kein Zugriff',
- '404': 'Seite nicht gefunden',
- '500': 'Serverfehler',
- description403: 'Sie haben keine Berechtigung, diese Seite zu sehen.',
- description404: 'Die Seite existiert nicht oder wurde verschoben.',
- description500: 'Auf dem Server ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.',
- },
- tour: {
- nextStep: 'Weiter',
- prevStep: 'Zurück',
- close: 'Tour beenden',
- skip: 'Überspringen',
- done: 'Fertig',
- progressText: (details: { current: number; total: number }) => `${details.current + 1} von ${details.total}`,
- },
- tableOfContents: { title: 'Auf dieser Seite' },
- spoiler: { show: 'Mehr anzeigen', hide: 'Weniger anzeigen' },
- navigationProgress: { label: 'Seite wird geladen' },
- copyButton: { label: 'Kopieren', copied: 'In die Zwischenablage kopiert' },
- actionBar: { label: 'Aktionen für die Auswahl', clear: 'Auswahl aufheben' },
- heatmap: { less: 'Weniger', more: 'Mehr', empty: 'kein Wert' },
- scheduler: {
- /** React Native: accessibility actions that move an event (±slot, ±day) */
- shift: (minutes: number, days: number) =>
- days
- ? `${Math.abs(days) === 1 ? '1 Tag' : `${Math.abs(days)} Tage`} ${days > 0 ? 'später' : 'früher'}`
- : `${Math.abs(minutes)} Minuten ${minutes > 0 ? 'später' : 'früher'}`,
- label: 'Kalender',
- today: 'Heute',
- previous: 'Zurück',
- next: 'Weiter',
- view: 'Ansicht',
- views: {
- day: 'Tag',
- week: 'Woche',
- month: 'Monat',
- agenda: 'Liste',
- timeline: 'Zeitachse',
- year: 'Jahr',
- } as Record,
- allDay: 'Ganztägig',
- more: (count: number) => `+${count} weitere`,
- newEvent: (date: string) => `Neuer Termin am ${date}`,
- empty: 'Keine Termine',
- changed: (title: string, when: string) => `${title}: ${when}`,
- /** Resources */
- resource: 'Ressource',
- allResources: 'Alle Ressourcen',
- previousResources: 'Vorherige Ressourcen',
- nextResources: 'Nächste Ressourcen',
- resourceRange: (from: number, to: number, count: number) => `Ressourcen ${from}–${to} von ${count}`,
- resourceFilter: 'Ressource',
- newEventFor: (resource: string, date: string) => `Neuer Termin für ${resource} am ${date}`,
- /** Timeline */
- range: 'Zeitraum',
- ranges: { day: 'Tag', week: 'Woche', month: 'Monat' } as Record,
- zoomIn: 'Vergrößern',
- zoomOut: 'Verkleinern',
- /** Series */
- series: 'Serientermin',
- seriesTitle: 'Serientermin ändern',
- seriesQuestion: (title: string) => `„${title}“ gehört zu einer Serie. Was soll sich ändern?`,
- onlyThis: 'Nur dieser Termin',
- allInSeries: 'Alle Termine der Serie',
- /** Rules for dropping */
- rejected: (title: string, reason: string) => `${title} lässt sich hier nicht ablegen: ${reason}`,
- notAllowed: 'nicht erlaubt',
- overlaps: (title: string) => `überschneidet sich mit ${title}`,
- /** Year view and "+N more" */
- eventCount: { one: '{count} Termin', other: '{count} Termine' } as PluralForms,
- closed: 'Außerhalb der Geschäftszeiten',
- loading: 'Termine werden geladen',
- loaded: { one: '{count} Termin geladen', other: '{count} Termine geladen' } as PluralForms,
- error: 'Die Termine konnten nicht geladen werden.',
- retry: 'Erneut versuchen',
- },
- recurrence: {
- DAILY: { one: 'Täglich', other: 'Alle {count} Tage' } as PluralForms,
- WEEKLY: { one: 'Wöchentlich', other: 'Alle {count} Wochen' } as PluralForms,
- MONTHLY: { one: 'Monatlich', other: 'Alle {count} Monate' } as PluralForms,
- YEARLY: { one: 'Jährlich', other: 'Alle {count} Jahre' } as PluralForms,
- on: (what: string) => `am ${what}`,
- /** "2. Montag", "letzten Freitag" */
- nthWeekday: (nth: number, weekday: string) =>
- nth === -1 ? `letzten ${weekday}` : nth < 0 ? `${-nth}.-letzten ${weekday}` : `${nth}. ${weekday}`,
- /** Monthly by position: "Jeden 2. Montag im Monat" */
- monthlyNth: (what: string) => `Jeden ${what} im Monat`,
- /** Day of the month: "15.", "letzten Tag" */
- monthDay: (day: number) => (day === -1 ? 'letzten Tag' : day < 0 ? `${-day}.-letzten Tag` : `${day}.`),
- /** Yearly: "2. Montag im Mai", "24. Dezember" */
- inMonth: (what: string, month: string) => `${what} im ${month}`,
- dayOfMonth: (day: string, month: string) => `${day} ${month}`,
- times: { one: 'einmal', other: '{count} Mal' } as PluralForms,
- until: (date: string) => `bis ${date}`,
- },
- gantt: {
- label: 'Projektplan',
- task: 'Aufgabe',
- start: 'Start',
- end: 'Ende',
- duration: 'Dauer',
- days: { one: '{count} Tag', other: '{count} Tage' } as PluralForms,
- milestone: 'Meilenstein',
- group: 'Gruppe',
- progress: (percent: number) => `${percent} % erledigt`,
- dependsOn: (names: string) => `hängt ab von ${names}`,
- critical: 'kritischer Pfad',
- zoom: 'Zoom',
- zooms: { day: 'Tag', week: 'Woche', month: 'Monat', quarter: 'Quartal' } as Record,
- today: 'Heute',
- week: (week: number) => `KW ${week}`,
- quarter: (quarter: number) => `Q${quarter}`,
- changed: (name: string, when: string) => `${name}: ${when}`,
- progressChanged: (name: string, percent: number) => `${name}: ${percent} % erledigt`,
- linked: (from: string, to: string) => `${to} hängt jetzt von ${from} ab`,
- addDependency: 'Abhängigkeit hinzufügen…',
- dependencyTitle: (name: string) => `Vorgänger von „${name}“`,
- predecessor: 'Vorgänger',
- noPredecessor: 'Keine Aufgabe möglich',
- addDependencyConfirm: 'Hinzufügen',
- empty: 'Keine Aufgaben',
- },
- codeHighlight: {
- copy: 'Code kopieren',
- showMore: 'Gesamten Code anzeigen',
- showLess: 'Code einklappen',
- wrap: 'Zeilen umbrechen',
- files: 'Dateien',
- added: 'hinzugefügt',
- removed: 'entfernt',
- },
- pdfViewer: {
- label: 'PDF-Dokument',
- toolbar: 'PDF-Werkzeuge',
- pages: 'Seiten des Dokuments',
- sidebar: 'Seitenleiste',
- thumbnails: 'Seiten',
- outline: 'Inhalt',
- noOutline: 'Kein Inhaltsverzeichnis',
- pageLabel: (page: number) => `Seite ${page}`,
- page: 'Seite',
- ofPages: (count: number) => `von ${count}`,
- pageStatus: (page: number, count: number) => `Seite ${page} von ${count}`,
- previousPage: 'Vorherige Seite',
- nextPage: 'Nächste Seite',
- zoom: 'Zoom',
- zoomIn: 'Vergrößern',
- zoomOut: 'Verkleinern',
- pageWidth: 'Seitenbreite',
- pageFit: 'Ganze Seite',
- rotate: 'Im Uhrzeigersinn drehen',
- search: 'Suchen',
- searchField: 'Im Dokument suchen',
- matchCase: 'Groß-/Kleinschreibung beachten',
- previousMatch: 'Vorheriger Treffer',
- nextMatch: 'Nächster Treffer',
- closeSearch: 'Suche schließen',
- matches: (current: number, total: number) => `Treffer ${current} von ${total}`,
- noMatches: 'Keine Treffer',
- searching: 'Wird gesucht …',
- wrappedTop: 'weiter am Ende',
- wrappedBottom: 'weiter am Anfang',
- download: 'Herunterladen',
- print: 'Drucken',
- printing: (page: number, count: number) => `Druck wird vorbereitet: Seite ${page} von ${count}`,
- fullscreen: 'Vollbild',
- loading: 'PDF wird geladen',
- loaded: (count: number) => (count === 1 ? 'PDF geladen, 1 Seite' : `PDF geladen, ${count} Seiten`),
- error: 'Das PDF konnte nicht geladen werden.',
- invalid: 'Die Datei ist kein gültiges PDF oder beschädigt.',
- missing: 'Das PDF wurde nicht gefunden.',
- passwordTitle: 'Geschütztes PDF',
- passwordDescription: 'Geben Sie das Passwort ein, um das Dokument zu öffnen.',
- passwordLabel: 'Passwort',
- passwordWrong: 'Das Passwort ist falsch. Bitte erneut versuchen.',
- passwordSubmit: 'Öffnen',
- passwordRequired: 'Ohne Passwort lässt sich das PDF nicht öffnen.',
- fileName: 'dokument.pdf',
- /** React Native: share or open the file; the web has no viewer for react-native-pdf */
- share: 'Teilen',
- unsupported: 'Die PDF-Ansicht gibt es nur in der App (iOS und Android).',
- },
- compare: {
- label: 'Vorher-Nachher-Vergleich',
- before: 'Vorher',
- after: 'Nachher',
- valueText: (percent: number) => `${percent} % vorher, ${100 - percent} % nachher`,
- },
- sankey: { source: 'Von', target: 'Nach', value: 'Wert' },
- treemap: { share: (value: string, percent: string) => `${value} · ${percent}` },
- countdown: {
- days: (count: number): string => (count === 1 ? 'Tag' : 'Tage'),
- hours: (count: number): string => (count === 1 ? 'Stunde' : 'Stunden'),
- minutes: (count: number): string => (count === 1 ? 'Minute' : 'Minuten'),
- seconds: (count: number): string => (count === 1 ? 'Sekunde' : 'Sekunden'),
- },
- rangePresets: {
- thisWeek: 'Diese Woche',
- lastWeek: 'Letzte Woche',
- thisMonth: 'Dieser Monat',
- lastMonth: 'Letzter Monat',
- thisQuarter: 'Dieses Quartal',
- lastQuarter: 'Letztes Quartal',
- thisYear: 'Dieses Jahr',
- lastYear: 'Letztes Jahr',
- last3Days: 'Letzte 3 Tage',
- last7Days: 'Letzte 7 Tage',
- last14Days: 'Letzte 14 Tage',
- last30Days: 'Letzte 30 Tage',
- last90Days: 'Letzte 90 Tage',
- last3Months: 'Letzte 3 Monate',
- last6Months: 'Letzte 6 Monate',
- last12Months: 'Letzte 12 Monate',
- last4Quarters: 'Letzte 4 Quartale',
- last3Years: 'Letzte 3 Jahre',
- last5Years: 'Letzte 5 Jahre',
- } as Record,
- splitter: { resize: (panel: string) => `Größe von „${panel}“ ändern` },
- image: {
- preview: (alt: string) => `Vergrößert anzeigen: ${alt}`,
- zoomIn: 'Vergrößern',
- zoomOut: 'Verkleinern',
- rotate: 'Drehen',
- close: 'Schließen',
- },
- inplace: { edit: 'bearbeiten' },
- scrollTop: { label: 'Nach oben' },
- blockUi: { label: 'Wird geladen' },
- speedDial: { label: 'Aktionen' },
- stepper: {
- prev: 'Zurück',
- next: 'Weiter',
- finish: 'Abschließen',
- /** React Native: status of a step and the progress */
- status: { done: 'abgeschlossen', active: 'aktueller Schritt', todo: 'offen' } as Record,
- progress: (step: number, count: number) => `Schritt ${step} von ${count}`,
- },
- dataTable: {
- row: (index: number) => `Zeile ${index + 1}`,
- selectAll: 'Alle Zeilen dieser Seite auswählen',
- selectRow: (row: string) => `${row} auswählen`,
- subRows: (row: string) => `Unterzeilen von ${row}`,
- search: (caption: string) => `${caption} durchsuchen`,
- searchPlaceholder: 'Suchen …',
- filter: (header: string) => `${header} filtern`,
- filterAll: 'Alle',
- columns: 'Spalten',
- export: 'Exportieren (CSV)',
- resize: (header: string) => `Breite von ${header}`,
- edit: (header: string, value: string) => `${value || 'leer'}, ${header} bearbeiten`,
- results: (count: number) => (count === 1 ? '1 Eintrag' : `${count} Einträge`),
- group: (header: string, value: string, count: number) => `${header}: ${value}, ${count} Einträge`,
- matchModes: {
- startsWith: 'Beginnt mit',
- contains: 'Enthält',
- notContains: 'Enthält nicht',
- endsWith: 'Endet mit',
- equals: 'Ist gleich',
- notEquals: 'Ist nicht gleich',
- lt: 'Kleiner als',
- lte: 'Kleiner oder gleich',
- gt: 'Größer als',
- gte: 'Größer oder gleich',
- between: 'Zwischen',
- in: 'Ist eins von',
- dateIs: 'Am',
- dateIsNot: 'Nicht am',
- dateBefore: 'Vor',
- dateAfter: 'Nach',
- } as Record,
- operator: 'Verknüpfung',
- matchAll: 'Alle Bedingungen erfüllt',
- matchAny: 'Eine Bedingung erfüllt',
- matchMode: (index: number) => `Bedingung ${index + 1}`,
- value: (index: number) => `Wert ${index + 1}`,
- from: 'Von',
- to: 'Bis',
- addRule: 'Bedingung hinzufügen',
- removeRule: (index: number) => `Bedingung ${index + 1} entfernen`,
- apply: 'Anwenden',
- clear: 'Zurücksetzen',
- filterMenu: (header: string, active: boolean) =>
- active ? `Filter für ${header}, aktiv` : `Filter für ${header}`,
- yes: 'Ja',
- no: 'Nein',
- columnMenu: (header: string) => `Optionen für ${header}`,
- sortAscending: 'Aufsteigend sortieren',
- sortDescending: 'Absteigend sortieren',
- clearSort: 'Sortierung entfernen',
- /** React Native: sheets for sorting and the column filters */
- sortBy: 'Sortieren',
- filters: 'Filter',
- sortPriority: (position: number, desc: boolean) =>
- `${position}. Sortierung, ${desc ? 'absteigend' : 'aufsteigend'}`,
- pinStart: 'Am Anfang fixieren',
- pinEnd: 'Am Ende fixieren',
- unpin: 'Nicht mehr fixieren',
- moveEarlier: 'Eine Spalte nach vorne',
- moveLater: 'Eine Spalte nach hinten',
- hideColumn: 'Ausblenden',
- groupBy: 'Nach dieser Spalte gruppieren',
- ungroup: 'Gruppierung aufheben',
- columnMoved: (header: string, position: number, count: number) =>
- `${header} ist jetzt Spalte ${position} von ${count}`,
- density: 'Zeilenhöhe',
- densities: { sm: 'Kompakt', md: 'Normal', lg: 'Großzügig' },
- fullscreen: 'Vollbild',
- exitFullscreen: 'Vollbild beenden',
- rowNumber: 'Nr.',
- expandAll: 'Alle Zeilen aufklappen',
- collapseAll: 'Alle Zeilen zuklappen',
- details: (row: string) => `Details zu ${row}`,
- actions: 'Aktionen',
- rowActions: (row: string) => `Aktionen für ${row}`,
- moveRow: (row: string) => `${row} verschieben`,
- rowMoved: (row: string, position: number, count: number) =>
- `${row} ist jetzt an Position ${position} von ${count}`,
- editRow: (row: string) => `${row} bearbeiten`,
- saveRow: (row: string) => `${row} speichern`,
- cancelEdit: (row: string) => `Bearbeiten von ${row} abbrechen`,
- rowsPerPage: 'Zeilen pro Seite',
- exportAll: 'Alle Zeilen',
- exportSelection: 'Ausgewählte Zeilen',
- filterToggle: 'Filter anzeigen',
- rowCount: (count: number) => (count === 1 ? '1 Zeile' : `${count} Zeilen`),
- filteredCount: (count: number) => `${count} gefiltert`,
- copy: (header: string, value: string) => `${value}, ${header} kopieren`,
- selectAllRows: 'Alle Zeilen auswählen',
- select: 'Auswahl',
- reorder: 'Reihenfolge',
- footer: 'Summenzeile',
- aggregates: {
- sum: (value: string) => `Summe ${value}`,
- avg: (value: string) => `Ø ${value}`,
- min: (value: string) => `Min. ${value}`,
- max: (value: string) => `Max. ${value}`,
- count: (value: string) => `Anzahl ${value}`,
- } as Record string>,
- exportMenu: 'Exportieren',
- exportXlsx: 'Exportieren (Excel)',
- exportAs: (format: 'csv' | 'xlsx', selection: boolean) =>
- `${selection ? 'Ausgewählte Zeilen' : 'Alle Zeilen'} als ${format === 'xlsx' ? 'Excel' : 'CSV'}`,
- cellsCopied: (count: number) => (count === 1 ? '1 Zelle kopiert' : `${count} Zellen kopiert`),
- cellsPasted: (count: number, invalid: number) =>
- `${count === 1 ? '1 Zelle' : `${count} Zellen`} eingefügt${invalid ? `, ${invalid} ungültig` : ''}`,
- pasteErrors: {
- number: 'Keine Zahl',
- date: 'Kein Datum',
- option: 'Keine der Auswahlmöglichkeiten',
- boolean: 'Weder Ja noch Nein',
- readonly: 'Nicht bearbeitbar',
- } as Record,
- changes: (count: number) => (count === 1 ? '1 Änderung' : `${count} Änderungen`),
- changed: 'geändert',
- saveChanges: 'Speichern',
- discardChanges: 'Verwerfen',
- changesSaved: (count: number) => (count === 1 ? '1 Änderung gespeichert' : `${count} Änderungen gespeichert`),
- changesDiscarded: 'Änderungen verworfen',
- undo: 'Rückgängig',
- redo: 'Wiederholen',
- undone: (count: number) => (count === 1 ? '1 Zelle zurückgesetzt' : `${count} Zellen zurückgesetzt`),
- redone: (count: number) => (count === 1 ? '1 Zelle wiederhergestellt' : `${count} Zellen wiederhergestellt`),
- rowMovedToGroup: (row: string, group: string, position: number, count: number) =>
- `${row} ist jetzt in ${group}, Position ${position} von ${count}`,
- rowMovedInTree: (row: string, parent: string | null, position: number, count: number) =>
- `${row} verschoben${parent ? ` nach ${parent}` : ''}, Position ${position} von ${count}`,
- find: 'In Tabelle suchen',
- findMatches: (current: number, total: number) => (total ? `Treffer ${current} von ${total}` : 'Keine Treffer'),
- findNext: 'Nächster Treffer',
- findPrevious: 'Vorheriger Treffer',
- findClose: 'Suche schließen',
- total: 'Gesamt',
- columnGroupEarlier: (header: string) => `${header} nach vorne`,
- columnGroupLater: (header: string) => `${header} nach hinten`,
- },
- dataSource: {
- loading: 'Wird geladen',
- loaded: { one: '{count} Eintrag geladen', other: '{count} Einträge geladen' } as PluralForms,
- empty: 'Keine Einträge gefunden',
- error: 'Die Daten konnten nicht geladen werden.',
- retry: 'Erneut versuchen',
- minSearch: (count: number) => `Bitte mindestens ${count} Zeichen eingeben`,
- /** Button at the end of a growing list */
- loadMore: 'Mehr laden',
- loadingMore: 'Weitere Einträge werden geladen',
- },
- dateRange: {
- start: 'Beginn',
- end: 'Ende',
- clearTrigger: 'Zeitraum löschen',
- },
- timePicker: {
- trigger: 'Uhrzeit wählen',
- clearTrigger: 'Uhrzeit löschen',
- /** TimeRangePicker */
- clearRange: 'Uhrzeiten löschen',
- },
- dateSegments: {
- day: 'Tag',
- month: 'Monat',
- year: 'Jahr',
- hour: 'Stunde',
- minute: 'Minute',
- second: 'Sekunde',
- dayPeriod: 'Tageszeit',
- } as Record,
- pinInput: { inputLabel: (index: number, length: number) => `Stelle ${index + 1} von ${length}` },
- rating: { ratingValueText: (index: number) => (index === 1 ? '1 Stern' : `${index} Sterne`) },
- carousel: {
- nextTrigger: 'Nächstes Bild',
- prevTrigger: 'Vorheriges Bild',
- indicator: (index: number) => `Bild ${index + 1} zeigen`,
- item: (index: number, count: number) => `Bild ${index + 1} von ${count}`,
- autoplayStart: 'Automatisch abspielen',
- autoplayStop: 'Abspielen anhalten',
- progressText: (details: { page: number; totalPages: number }) => `${details.page + 1} / ${details.totalPages}`,
- },
- chart: {
- showTable: 'Daten als Tabelle',
- /** Header of the label column */
- category: 'Kategorie',
- sparkline: (label: string, first: string, last: string, min: string, max: string) =>
- `${label}: von ${first} auf ${last}, Tiefstwert ${min}, Höchstwert ${max}`,
- /** Column of a dataset without label */
- series: (index: number) => `Reihe ${index + 1}`,
- /** Waterfall: a step and the running sum */
- change: 'Veränderung',
- total: 'Summe',
- /** Brush: the shown range */
- brush: 'Ausschnitt',
- brushThumbs: ['Beginn', 'Ende'],
- legend: 'Legende',
- share: 'Anteil',
- value: 'Wert',
- references: 'Referenzwerte',
- /** Funnel: stage and conversion */
- stage: 'Schritt',
- conversion: 'Umwandlung',
- fromPrevious: (percent: string) => `${percent} vom vorherigen Schritt`,
- /** Bullet chart: value, maximum and target */
- target: 'Ziel',
- bulletValue: (value: string, max: string, target?: string) =>
- target ? `${value} von ${max}, Ziel ${target}` : `${value} von ${max}`,
- },
- editor: {
- toolbar: 'Formatierung',
- bold: 'Fett',
- italic: 'Kursiv',
- underline: 'Unterstrichen',
- strike: 'Durchgestrichen',
- heading2: 'Überschrift',
- heading3: 'Zwischenüberschrift',
- bulletList: 'Aufzählung',
- orderedList: 'Nummerierte Liste',
- blockquote: 'Zitat',
- code: 'Code',
- link: 'Link',
- undo: 'Rückgängig',
- redo: 'Wiederholen',
- linkUrl: 'Linkadresse',
- linkApply: 'Übernehmen',
- linkRemove: 'Link entfernen',
- linkCancel: 'Abbrechen',
- taskList: 'Aufgabenliste',
- codeBlock: 'Codeblock',
- horizontalRule: 'Trennlinie',
- image: 'Bild',
- imageUrl: 'Bildadresse',
- imageAlt: 'Alternativtext',
- imageDecorative: 'Nur Schmuck (ohne Alternativtext)',
- imageUpload: 'Datei wählen',
- imageInsert: 'Einfügen',
- imageUpdate: 'Übernehmen',
- table: 'Tabelle',
- tableBar: 'Tabelle bearbeiten',
- tableActions: {
- insertTable: 'Tabelle einfügen',
- addRowBefore: 'Zeile darüber',
- addRowAfter: 'Zeile darunter',
- addColumnBefore: 'Spalte davor',
- addColumnAfter: 'Spalte danach',
- deleteRow: 'Zeile löschen',
- deleteColumn: 'Spalte löschen',
- toggleHeaderRow: 'Kopfzeile ein/aus',
- mergeOrSplit: 'Zellen verbinden/teilen',
- deleteTable: 'Tabelle löschen',
- } as Record,
- taskItem: (text: string, checked: boolean) => `${text || 'Leere Aufgabe'}, ${checked ? 'erledigt' : 'offen'}`,
- shortcut: (letter: string, shift: boolean) => `Strg+${shift ? 'Umschalt+' : ''}${letter}`,
- alignLeft: 'Linksbündig',
- alignCenter: 'Zentriert',
- alignRight: 'Rechtsbündig',
- alignJustify: 'Blocksatz',
- textColor: 'Textfarbe',
- backgroundColor: 'Hintergrundfarbe',
- colorDefault: 'Standard',
- colors: {
- primary: 'Akzent',
- info: 'Info',
- success: 'Erfolg',
- warn: 'Warnung',
- danger: 'Fehler',
- muted: 'Gedämpft',
- } as Record,
- subscript: 'Tiefgestellt',
- superscript: 'Hochgestellt',
- source: 'HTML-Quelltext',
- sourceField: 'HTML-Quelltext bearbeiten',
- details: 'Aufklappbarer Abschnitt',
- detailsToggle: (open: boolean): string => (open ? 'Abschnitt zuklappen' : 'Abschnitt aufklappen'),
- invisibleCharacters: 'Unsichtbare Zeichen',
- tableOfContents: 'Inhaltsverzeichnis',
- characters: (count: number, limit?: number) =>
- limit === undefined ? `${count} Zeichen` : `${count} von ${limit} Zeichen`,
- words: (count: number) => (count === 1 ? '1 Wort' : `${count} Wörter`),
- limitReached: 'Höchstlänge erreicht',
- imageUploading: 'Bild wird hochgeladen …',
- imageUploaded: 'Bild eingefügt',
- imageFailed: 'Das Bild konnte nicht hochgeladen werden',
- bubbleMenu: 'Auswahl formatieren',
- floatingMenu: 'Block einfügen',
- menuHint: 'Alt+F10: Menü',
- suggestions: 'Vorschläge',
- noSuggestions: 'Keine Treffer',
- suggestionCount: (count: number) => (count === 1 ? '1 Vorschlag' : `${count} Vorschläge`),
- mentioned: (label: string) => `${label} erwähnt`,
- },
- gallery: {
- fullscreen: 'Vollbild',
- close: 'Vollbild schließen',
- thumbnails: 'Vorschaubilder',
- thumbnail: (index: number, count: number) => `Bild ${index + 1} von ${count} zeigen`,
- /** Lightbox toolbar (fullscreen view) */
- toolbar: 'Bildwerkzeuge',
- zoomIn: 'Vergrößern',
- zoomOut: 'Verkleinern',
- resetZoom: 'Originalgröße',
- rotate: 'Drehen',
- download: 'Herunterladen',
- screen: 'Bildschirmfüllend',
- exitScreen: 'Bildschirmfüllend beenden',
- zoom: (percent: number) => `Zoom ${percent} %`,
- /** Keyboard help of the lightbox */
- shortcuts: 'Pfeiltasten: Bild wechseln · + und −: Zoom · 0: Originalgröße · R: Drehen · F: Bildschirmfüllend',
- video: 'Video',
- },
- /** Ark names the colour picker parts in English and has no translations for them */
- colorPicker: {
- trigger: (color: string) => `Farbe wählen, aktuell ${color}`,
- /** Name of the trigger without a Field or aria-label */
- name: 'Farbe',
- area: 'Sättigung und Helligkeit',
- hue: 'Farbton',
- alpha: 'Deckkraft',
- hex: 'Hex-Wert',
- swatch: (color: string) => `${color} wählen`,
- /** ColorInput: the swatch button that opens the picker */
- open: 'Farbwähler öffnen',
- eyeDropper: 'Farbe vom Bildschirm aufnehmen',
- format: 'Format',
- formats: { rgb: 'RGB', hsl: 'HSL', hsb: 'HSB', oklch: 'OKLCH' } as Record,
- /** Names of the channel fields per format */
- channels: {
- rgb: ['Rot', 'Grün', 'Blau'],
- hsl: ['Farbton', 'Sättigung', 'Helligkeit'],
- hsb: ['Farbton', 'Sättigung', 'Hellwert'],
- oklch: ['Helligkeit', 'Buntheit', 'Farbton'],
- } as Record,
- palette: 'Farben',
- },
- tree: {
- treeLabel: 'Baum',
- moved: (label: string, parent: string | null, position: number, count: number) =>
- `${label} verschoben${parent ? ` nach ${parent}` : ''}, Position ${position} von ${count}`,
- /** Row below a branch while its children load */
- loadingRow: 'Wird geladen …',
- loadingNode: (label: string) => `${label}: Wird geladen`,
- loadedNode: {
- one: '{label}: {count} Eintrag geladen',
- other: '{label}: {count} Einträge geladen',
- } as PluralForms,
- loadError: (label: string) => `${label} konnte nicht geladen werden`,
- /** Row below a branch whose children failed to load; activating it loads again */
- retry: 'Laden fehlgeschlagen – erneut versuchen',
- filter: 'Baum durchsuchen',
- matches: { one: '{count} Treffer', other: '{count} Treffer' } as PluralForms,
- noMatches: 'Keine Treffer',
- expandAll: 'Alle aufklappen',
- collapseAll: 'Alle zuklappen',
- cut: (label: string) => `${label} ausgeschnitten – mit Strg+V an einem anderen Knoten einfügen`,
- cutCancelled: 'Ausschneiden abgebrochen',
- dropDenied: 'Hier nicht möglich',
- },
- treeSelect: {
- search: 'Suchen',
- /** Chip for the selected nodes beyond `maxShown` */
- more: (count: number) => `+${count}`,
- moreLabel: (count: number) => `${count} weitere ausgewählt`,
- },
- cascadeSelect: {
- search: 'Suchen',
- /** Button next to the search field that opens the levels */
- showLevels: 'Ebenen anzeigen',
- /** First entry of a level with `changeOnSelect`: chooses the level itself */
- choose: (label: string) => `${label} wählen`,
- /** A hit of the search: the whole path */
- path: (labels: string[]) => labels.join(' › '),
- },
- timeline: {
- loadMore: 'Ältere Einträge laden',
- loading: 'Ältere Einträge werden geladen',
- },
- orgChart: {
- expand: (label: string) => `Team von ${label} anzeigen`,
- collapse: (label: string) => `Team von ${label} ausblenden`,
- loading: (label: string) => `${label}: Wird geladen`,
- retry: (label: string) => `${label}: Laden fehlgeschlagen – erneut versuchen`,
- },
- fileUpload: {
- dropzone: 'Dateien ablegen',
- itemPreview: (file: File) => `Vorschau von ${file.name}`,
- deleteFile: (file: File) => `${file.name} entfernen`,
- dropzoneText: (multiple: boolean): string =>
- multiple ? 'Dateien hierher ziehen oder' : 'Datei hierher ziehen oder',
- trigger: (multiple: boolean): string => (multiple ? 'Dateien auswählen' : 'Datei auswählen'),
- /** Messages for rejected files (Ark FileUpload error codes) */
- errors: {
- TOO_MANY_FILES: 'Zu viele Dateien',
- FILE_INVALID_TYPE: 'Dateityp nicht erlaubt',
- FILE_TOO_LARGE: 'Datei ist zu groß',
- FILE_TOO_SMALL: 'Datei ist zu klein',
- FILE_INVALID: 'Datei ist ungültig',
- FILE_EXISTS: 'Datei ist schon ausgewählt',
- } as Record,
- upload: 'Hochladen',
- uploadAll: 'Alle hochladen',
- cancel: (file: File) => `Hochladen von ${file.name} abbrechen`,
- retry: (file: File) => `${file.name} erneut hochladen`,
- progress: (file: File) => `Fortschritt von ${file.name}`,
- status: {
- pending: 'Bereit zum Hochladen',
- uploading: (percent: number) => `Wird hochgeladen … ${percent} %`,
- done: 'Hochgeladen',
- error: 'Hochladen fehlgeschlagen',
- cancelled: 'Abgebrochen',
- },
- /** Announced when an upload ends */
- uploaded: (file: File) => `${file.name} hochgeladen`,
- failed: (file: File) => `${file.name} konnte nicht hochgeladen werden`,
- dropAccept: (multiple: boolean): string =>
- multiple ? 'Loslassen, um die Dateien hinzuzufügen' : 'Loslassen, um die Datei hinzuzufügen',
- dropReject: 'Diese Dateien sind hier nicht erlaubt',
- fullscreen: 'Dateien irgendwo ablegen',
- /** React Native: where FileUpload takes files from (the pickers of the app) */
- sources: { documents: 'Dokument wählen', photos: 'Aus Fotos wählen', camera: 'Foto aufnehmen' },
- pasted: (count: number) => (count === 1 ? '1 Datei eingefügt' : `${count} Dateien eingefügt`),
- },
- statistic: { loading: 'Wird geladen' },
- truncate: { more: 'Mehr anzeigen', less: 'Weniger anzeigen' },
- overflowList: {
- /** Hidden part of the "+N" button's name (after the visible "+N") */
- more: 'weitere',
- title: 'Weitere Einträge',
- },
- toggletip: {
- label: 'Weitere Informationen',
- labelFor: (label: string) => `Weitere Informationen zu ${label}`,
- },
- checkboxGroup: { maxReached: (max: number) => `Höchstens ${max} auswählbar` },
- skipLinks: { label: 'Sprunglinks' },
- hotkeys: {
- /** Key names for the display (Kbd `hotkey`, HotkeysDialog); Apple devices show symbols */
- keys: {
- ctrl: 'Strg',
- alt: 'Alt',
- shift: 'Umschalt',
- meta: 'Win',
- space: 'Leertaste',
- enter: 'Eingabe',
- escape: 'Esc',
- backspace: 'Rücktaste',
- delete: 'Entf',
- tab: 'Tab',
- },
- /** Between the steps of a sequence ("G dann I") */
- then: 'dann',
- title: 'Tastenkürzel',
- description: 'Alle Tastenkürzel dieser Seite',
- empty: 'Keine Tastenkürzel',
- /** Description of the "?" shortcut */
- open: 'Tastenkürzel anzeigen',
- /** Group of shortcuts without their own */
- general: 'Allgemein',
- /** Description of the CommandPalette's shortcut */
- commandPalette: 'Befehle öffnen',
- },
- persona: {
- presence: { online: 'Online', away: 'Abwesend', busy: 'Beschäftigt', offline: 'Offline' } as Record<
- string,
- string
- >,
- },
- inlineEdit: {
- edit: 'bearbeiten',
- save: 'Speichern',
- cancel: 'Abbrechen',
- empty: 'Leer',
- saving: 'Wird gespeichert …',
- /** Title of the popover */
- title: (label: string) => `${label} bearbeiten`,
- },
- searchField: { label: 'Suche', placeholder: 'Suchen …', clear: 'Suche löschen', scope: 'Suchbereich' },
- filterPanel: {
- label: 'Filter',
- toggle: 'Filter',
- /** Hidden part of the toggle's name after the visible count */
- active: 'aktiv',
- status: (count: number) =>
- count === 0 ? 'Keine Filter aktiv' : count === 1 ? '1 Filter aktiv' : `${count} Filter aktiv`,
- },
- filterChips: {
- label: 'Aktive Filter',
- remove: (label: string) => `Filter ${label} entfernen`,
- reset: 'Alle zurücksetzen',
- },
- conditionBuilder: {
- label: 'Bedingungen',
- combinator: 'Verknüpfung',
- and: 'Und',
- or: 'Oder',
- all: 'Alle Bedingungen erfüllt',
- any: 'Eine Bedingung erfüllt',
- not: 'Nicht',
- group: (path: string) => `Gruppe ${path}`,
- rule: (path: string) => `Bedingung ${path}`,
- addRule: 'Bedingung hinzufügen',
- addGroup: 'Gruppe hinzufügen',
- removeRule: (path: string) => `Bedingung ${path} entfernen`,
- removeGroup: (path: string) => `Gruppe ${path} entfernen`,
- field: 'Feld',
- operator: 'Operator',
- value: 'Wert',
- from: 'Von',
- to: 'Bis',
- yes: 'Ja',
- no: 'Nein',
- empty: 'Keine Bedingungen – alle Einträge passen',
- /** Operators beyond the DataTable match modes */
- operators: { empty: 'Ist leer', notEmpty: 'Ist nicht leer', in: 'Ist eins von' } as Record,
- added: 'Bedingung hinzugefügt',
- groupAdded: 'Gruppe hinzugefügt',
- removed: 'Bedingung entfernt',
- groupRemoved: 'Gruppe entfernt',
- /** The DataTable's toggle */
- advanced: 'Erweiterter Filter',
- },
- formList: {
- add: 'Eintrag hinzufügen',
- item: (position: number) => `Eintrag ${position}`,
- remove: (item: string) => `${item} entfernen`,
- moveUp: (item: string) => `${item} nach oben`,
- moveDown: (item: string) => `${item} nach unten`,
- added: (item: string) => `${item} hinzugefügt`,
- removed: (item: string) => `${item} entfernt`,
- moved: (item: string, position: number) => `${item} an Position ${position} verschoben`,
- max: (max: number) => `Höchstens ${max} Einträge`,
- min: (min: number) => `Mindestens ${min} Einträge`,
- },
- keyValueEditor: {
- key: 'Schlüssel',
- value: 'Wert',
- add: 'Zeile hinzufügen',
- item: (position: number) => `Zeile ${position}`,
- duplicate: 'Schlüssel kommt mehrfach vor',
- },
- wizard: {
- summary: 'Zusammenfassung',
- validating: 'Wird geprüft …',
- invalid: 'Bitte die Angaben in diesem Schritt prüfen',
- },
- aiLabel: {
- label: 'KI-generiert',
- explanation:
- 'Dieser Inhalt wurde mit künstlicher Intelligenz erstellt. Bitte prüfen Sie ihn vor der Verwendung.',
- help: 'Was bedeutet KI-generiert?',
- },
- qrCode: {
- label: (value: string) => `QR-Code: ${value}`,
- downloadPng: 'Als PNG herunterladen',
- downloadSvg: 'Als SVG herunterladen',
- },
- signaturePad: {
- label: 'Unterschrift',
- roleDescription: 'Unterschriftsfeld',
- control: 'Unterschriftsfeld – mit Maus, Stift oder Finger unterschreiben',
- placeholder: 'Hier unterschreiben',
- clear: 'Löschen',
- undo: 'Rückgängig',
- type: 'Unterschrift eintippen',
- draw: 'Unterschrift zeichnen',
- typedLabel: 'Name als Unterschrift',
- cleared: 'Unterschrift gelöscht',
- },
- imageCropper: {
- label: 'Bild zuschneiden',
- roleDescription: 'Bildzuschnitt',
- loading: 'Bild wird geladen',
- description: (zoom: number | null, rotation: number | null) =>
- `Zoom ${Math.round((zoom ?? 1) * 100)} %, gedreht um ${Math.round(rotation ?? 0)} Grad`,
- selection: (circle: boolean): string => (circle ? 'Kreisförmiger Ausschnitt' : 'Ausschnitt'),
- selectionRoleDescription: 'verschiebbarer Ausschnitt',
- instructions:
- 'Pfeiltasten verschieben den Ausschnitt, Alt und Pfeiltasten ändern seine Größe, Plus und Minus zoomen.',
- selectionValue: (width: number, height: number, x: number, y: number) =>
- `${width} × ${height} Pixel, Position ${x}, ${y}`,
- zoomIn: 'Vergrößern',
- zoomOut: 'Verkleinern',
- rotateLeft: 'Nach links drehen',
- rotateRight: 'Nach rechts drehen',
- flip: 'Horizontal spiegeln',
- reset: 'Zurücksetzen',
- toolbar: 'Bild bearbeiten',
- },
- floatingPanel: {
- minimize: 'Minimieren',
- maximize: 'Maximieren',
- restore: 'Wiederherstellen',
- close: 'Schließen',
- hint: 'Alt und Pfeiltasten verschieben das Fenster, Alt, Umschalt und Pfeiltasten ändern seine Größe.',
- },
- jsonViewer: {
- label: 'JSON-Daten',
- search: 'Durchsuchen',
- copy: (path: string) => `${path} kopieren`,
- copied: (path: string) => `${path} kopiert`,
- items: (count: number) => (count === 1 ? '1 Eintrag' : `${count} Einträge`),
- keys: (count: number) => (count === 1 ? '1 Schlüssel' : `${count} Schlüssel`),
- matches: (count: number) => (count === 0 ? 'Keine Treffer' : count === 1 ? '1 Treffer' : `${count} Treffer`),
- types: {
- object: 'Objekt',
- array: 'Liste',
- string: 'Text',
- number: 'Zahl',
- boolean: 'Wahrheitswert',
- null: 'leer',
- undefined: 'undefiniert',
- } as Record,
- },
- jsonInput: {
- format: 'Formatieren',
- valid: 'Gültiges JSON',
- invalid: 'Ungültiges JSON',
- invalidAt: (line: number, column: number) => `Ungültiges JSON in Zeile ${line}, Spalte ${column}`,
- },
- downloadTrigger: { label: 'Herunterladen' },
- marquee: { label: 'Laufband', pause: 'Laufband anhalten', play: 'Laufband fortsetzen' },
- angleSlider: { label: 'Winkel', value: (degrees: number) => `${degrees} Grad` },
- mentions: {
- label: 'Nachricht',
- suggestions: 'Vorschläge',
- /** Announced when the suggestions open or change */
- count: (count: number) =>
- count === 0 ? 'Keine Vorschläge' : count === 1 ? '1 Vorschlag' : `${count} Vorschläge`,
- hint: (triggers: string) => `${triggers} öffnet Vorschläge, Pfeiltasten wählen, Eingabe übernimmt`,
- },
- phoneInput: {
- country: 'Land',
- number: 'Telefonnummer',
- search: 'Land suchen',
- },
- colorSwatchPicker: { label: 'Farbe' },
- emojiPicker: {
- /**
- * Names and search words of the emojis in this language, one per line: "😀 Name|search words" – empty uses the
- * built-in German (de) or English names; the English words always find, too
- */
- emojis: '',
- label: 'Emojis',
- search: 'Emoji suchen',
- categoryNav: 'Kategorien',
- categories: {
- recent: 'Zuletzt verwendet',
- smileys: 'Smileys & Gefühle',
- people: 'Menschen & Gesten',
- nature: 'Tiere & Natur',
- food: 'Essen & Trinken',
- travel: 'Reisen & Orte',
- activities: 'Aktivitäten',
- objects: 'Objekte',
- symbols: 'Symbole',
- } as Record,
- results: 'Suchergebnisse',
- empty: 'Keine Emojis gefunden',
- count: (count: number) => (count === 0 ? 'Keine Emojis' : count === 1 ? '1 Emoji' : `${count} Emojis`),
- },
- ringProgress: { label: 'Fortschritt' },
- sortable: {
- instructions:
- 'Leertaste oder Eingabetaste nimmt auf. Dann mit den Pfeiltasten verschieben, mit Leertaste ablegen oder mit Escape abbrechen. Mit der Maus: ziehen, oder klicken und an der Zielstelle erneut klicken.',
- handle: (item: string) => `${item} verschieben`,
- empty: 'Keine Einträge – hierher verschieben',
- picked: (item: string, position: number, count: number) =>
- `${item} aufgenommen, Position ${position} von ${count}.`,
- moved: (item: string, position: number, count: number) => `${item}: Position ${position} von ${count}.`,
- movedTo: (item: string, container: string, position: number, count: number) =>
- `${item} in ${container}, Position ${position} von ${count}.`,
- dropped: (item: string, position: number, count: number) =>
- `${item} abgelegt, Position ${position} von ${count}.`,
- cancelled: (item: string, position: number, count: number) =>
- `Verschieben abgebrochen. ${item} wieder an Position ${position} von ${count}.`,
- },
- logViewer: {
- label: 'Protokoll',
- search: 'Protokoll durchsuchen',
- levels: { debug: 'Debug', info: 'Info', warn: 'Warnung', error: 'Fehler' } as Record,
- levelFilter: 'Ebenen',
- autoScroll: 'Mitlaufen',
- loadMore: 'Ältere Einträge laden',
- jumpToEnd: 'Zum Ende',
- lines: (shown: number, total: number) => (shown === total ? `${total} Zeilen` : `${shown} von ${total} Zeilen`),
- empty: 'Keine Einträge',
- paused: 'Mitlaufen angehalten',
- },
- kanban: {
- /** React Native: action that moves a card to another column */
- moveTo: (column: string) => `Nach „${column}“ verschieben`,
- label: 'Board',
- count: (count: number, limit?: number) => (limit ? `${count}/${limit}` : String(count)),
- cards: (count: number, limit?: number) =>
- `${count === 1 ? '1 Karte' : `${count} Karten`}${limit ? `, Limit ${limit}` : ''}`,
- limitReached: 'WIP-Limit erreicht',
- limitExceeded: (count: number, limit: number) => `WIP-Limit überschritten: ${count} von ${limit}`,
- collapse: (column: string) => `${column} einklappen`,
- expand: (column: string) => `${column} ausklappen`,
- loadMore: 'Weitere laden',
- empty: 'Keine Karten – hierher verschieben',
- },
- chatMessages: {
- label: 'Nachrichten',
- empty: 'Noch keine Nachrichten',
- jump: 'Zu neuen Nachrichten',
- newCount: (count: number) => (count === 1 ? '1 neue Nachricht' : `${count} neue Nachrichten`),
- announce: (author: string, text: string) => `${author}: ${text}`,
- today: 'Heute',
- yesterday: 'Gestern',
- loadMore: 'Ältere Nachrichten laden',
- },
- chatMessage: {
- status: { sending: 'Wird gesendet', sent: 'Gesendet', read: 'Gelesen', error: 'Nicht gesendet' } as Record<
- string,
- string
- >,
- writing: 'Antwort wird erstellt',
- attachments: 'Anhänge',
- you: 'Sie',
- assistant: 'Assistent',
- },
- chatPrompt: {
- label: 'Nachricht',
- placeholder: 'Nachricht schreiben …',
- send: 'Senden',
- stop: 'Antwort stoppen',
- attach: 'Datei anhängen',
- removeFile: (name: string) => `${name} entfernen`,
- hint: 'Enter sendet, Umschalt+Enter für eine neue Zeile',
- hintModEnter: 'Strg+Enter sendet',
- },
- chatSources: { title: (count: number) => (count === 1 ? '1 Quelle' : `${count} Quellen`) },
- chatToolCall: {
- title: (name: string) => `Werkzeug: ${name}`,
- status: { running: 'Läuft …', done: 'Fertig', error: 'Fehler' } as Record,
- input: 'Eingabe',
- output: 'Ergebnis',
- },
- chatThinking: {
- active: 'Denkt nach …',
- done: 'Gedankengang',
- duration: (seconds: number) => `${seconds} s nachgedacht`,
- },
- authForm: {
- titles: {
- signIn: 'Anmelden',
- signUp: 'Konto erstellen',
- reset: 'Passwort zurücksetzen',
- code: 'Code eingeben',
- } as Record,
- descriptions: {
- signIn: 'Mit E-Mail-Adresse und Passwort',
- signUp: 'In wenigen Schritten zum eigenen Konto',
- reset: 'Wir schicken Ihnen einen Link zum Zurücksetzen.',
- code: 'Den Code aus der E-Mail oder der Authentifizierungs-App eingeben',
- } as Record,
- submit: { signIn: 'Anmelden', signUp: 'Registrieren', reset: 'Link senden', code: 'Bestätigen' } as Record<
- string,
- string
- >,
- name: 'Name',
- email: 'E-Mail-Adresse',
- password: 'Passwort',
- passwordHint: (min: number) => `Mindestens ${min} Zeichen`,
- code: 'Einmalcode',
- remember: 'Angemeldet bleiben',
- forgot: 'Passwort vergessen?',
- or: 'oder',
- providers: 'Anmelden mit',
- noAccount: 'Noch kein Konto?',
- signUpLink: 'Registrieren',
- hasAccount: 'Schon ein Konto?',
- signInLink: 'Anmelden',
- back: 'Zurück zur Anmeldung',
- resetSent: 'Falls ein Konto zu dieser Adresse existiert, ist ein Link unterwegs.',
- sending: 'Wird gesendet …',
- },
- sessionTimeout: {
- title: 'Sitzung läuft bald ab',
- description: 'Aus Sicherheitsgründen werden Sie bei Inaktivität abgemeldet. Möchten Sie angemeldet bleiben?',
- remaining: 'Verbleibende Zeit',
- extend: 'Angemeldet bleiben',
- signOut: 'Abmelden',
- expiredTitle: 'Sitzung abgelaufen',
- expired: 'Sie wurden aus Sicherheitsgründen abgemeldet.',
- close: 'Schließen',
- },
- /** Messages of the validators (isRequired, isEmail …) */
- validation: {
- required: 'Pflichtfeld',
- email: 'Bitte eine gültige E-Mail-Adresse eingeben',
- url: 'Bitte eine gültige Adresse eingeben (https://…)',
- minLength: (min: number) => `Mindestens ${min} Zeichen`,
- maxLength: (max: number) => `Höchstens ${max} Zeichen`,
- length: (min: number, max: number) => `${min} bis ${max} Zeichen`,
- min: (min: string) => `Mindestens ${min}`,
- max: (max: string) => `Höchstens ${max}`,
- range: (min: string, max: string) => `Zwischen ${min} und ${max}`,
- number: 'Bitte eine Zahl eingeben',
- integer: 'Bitte eine ganze Zahl eingeben',
- pattern: 'Das Format stimmt nicht',
- matches: 'Die Eingaben stimmen nicht überein',
- iban: 'Bitte eine gültige IBAN eingeben',
- phone: 'Bitte eine gültige Telefonnummer eingeben',
- },
-};
+type CalendarView = 'day' | 'month' | 'year';
/** All texts of one language; a custom language passes a complete set to LocaleProvider */
export type Translations = typeof de;
@@ -2492,7 +1192,8 @@ const en: Translations = {
};
/** Built-in languages; others fall back to English unless registered (registerLocale) or passed to LocaleProvider */
-export const messages: Record<'de' | 'en', Translations> = { de, en };
+/** The built-in language – German and the others are registered (registerLocale) */
+export const messages: Record<'en', Translations> = { en };
const registered = new Map();
@@ -2685,6 +1386,14 @@ export function numberSeparators(locale = defaultLocale): { decimal: string; gro
};
}
+/**
+ * CSV separator a spreadsheet in the locale opens: ";" where numbers have a decimal comma ("de-DE": 1,5 – the comma
+ * would split the number), "," otherwise
+ */
+export function csvSeparator(locale = defaultLocale): string {
+ return numberSeparators(locale).decimal === ',' ? ';' : ',';
+}
+
/**
* Reads a number typed or pasted in either notation – "1.234,5", "1,234.5", "1234,5" and "1234.5" all give 1234.5.
* With both separators the last one is the decimal separator; a single separator before exactly three digits is a
diff --git a/packages/recipes/src/index.ts b/packages/recipes/src/index.ts
index 7b64e68..8eed1a5 100644
--- a/packages/recipes/src/index.ts
+++ b/packages/recipes/src/index.ts
@@ -412,6 +412,7 @@ export {
localizeTexts,
messages,
numberInputKey,
+ csvSeparator,
numberSeparators,
parseLocaleNumber,
pluralText,
diff --git a/packages/recipes/src/locales/de.ts b/packages/recipes/src/locales/de.ts
new file mode 100644
index 0000000..40415fa
--- /dev/null
+++ b/packages/recipes/src/locales/de.ts
@@ -0,0 +1,1815 @@
+// German texts of the components – they define the shape of every language (Translations in ../i18n.ts). Not built
+// in, so they cost nothing until imported: registerLocale('de', de) at app start, or
+// .
+import type { PluralForms } from '../i18n.js';
+
+type CalendarView = 'day' | 'month' | 'year';
+
+const next: Record = {
+ day: 'Nächster Monat',
+ month: 'Nächstes Jahr',
+ year: 'Nächstes Jahrzehnt',
+};
+const previous: Record = {
+ day: 'Vorheriger Monat',
+ month: 'Vorheriges Jahr',
+ year: 'Vorheriges Jahrzehnt',
+};
+const viewNames: Record = { day: 'Tage', month: 'Monate', year: 'Jahre' };
+
+export const de = {
+ common: {
+ close: 'Schließen',
+ cancel: 'Abbrechen',
+ confirm: 'Bestätigen',
+ placeholder: 'Bitte wählen',
+ loading: 'Wird geladen',
+ empty: 'Keine Einträge',
+ noResults: 'Keine Treffer',
+ selectedCount: (count: number) => `${count} ausgewählt`,
+ },
+ toast: { label: 'Benachrichtigungen' },
+ menu: { title: 'Aktionen', search: 'Menü durchsuchen' },
+ menubar: { menu: 'Menü' },
+ dialog: { maximize: 'Maximieren', restore: 'Verkleinern' },
+ /** confirm() and prompt() */
+ confirm: {
+ typeToConfirm: (text: string) => `Zur Bestätigung „${text}“ eingeben`,
+ mismatch: 'Die Eingabe stimmt nicht überein',
+ },
+ breadcrumb: { label: 'Pfad', more: 'Ausgeblendete Ebenen' },
+ siteHeader: { navigation: 'Hauptnavigation', menu: 'Menü' },
+ modeToggle: { light: 'Helles Design aktivieren', dark: 'Dunkles Design aktivieren' },
+ pagination: {
+ rootLabel: 'Seitennavigation',
+ prevTriggerLabel: 'Vorherige Seite',
+ nextTriggerLabel: 'Nächste Seite',
+ itemLabel: (details: { page: number; totalPages: number }) => `Seite ${details.page} von ${details.totalPages}`,
+ summary: (range: { start: number; end: number; count: number }) =>
+ `${range.start}–${range.end} von ${range.count}`,
+ firstTriggerLabel: 'Erste Seite',
+ lastTriggerLabel: 'Letzte Seite',
+ /** Compact variant: the current page between the arrows */
+ compact: (page: number, totalPages: number) => `Seite ${page} von ${totalPages}`,
+ pageSize: 'Pro Seite',
+ jumper: 'Gehe zu Seite',
+ },
+ saveStatus: {
+ idle: '',
+ saving: 'Wird gespeichert …',
+ saved: 'Gespeichert',
+ error: 'Speichern fehlgeschlagen',
+ },
+ slider: { thumbs: ['Minimum', 'Maximum'] },
+ numberInput: { incrementLabel: 'Erhöhen', decrementLabel: 'Verringern' },
+ passwordInput: {
+ visibilityTrigger: (visible: boolean): string => (visible ? 'Passwort verbergen' : 'Passwort anzeigen'),
+ },
+ combobox: { triggerLabel: 'Vorschläge anzeigen', clearTriggerLabel: 'Eingabe löschen', search: 'Suchen …' },
+ datePicker: {
+ dayCell: (state: { valueText: string; selected: boolean; unavailable: boolean }) =>
+ state.unavailable
+ ? `Nicht verfügbar: ${state.valueText}`
+ : state.selected
+ ? `Ausgewählt: ${state.valueText}`
+ : state.valueText,
+ nextTrigger: (view: CalendarView) => next[view],
+ prevTrigger: (view: CalendarView) => previous[view],
+ viewTrigger: (_view: CalendarView, nextView: CalendarView | undefined) =>
+ nextView ? `${viewNames[nextView]} anzeigen` : 'Ansicht wechseln',
+ monthSelect: 'Monat wählen',
+ yearSelect: 'Jahr wählen',
+ clearTrigger: 'Datum löschen',
+ trigger: (open: boolean): string => (open ? 'Kalender schließen' : 'Kalender öffnen'),
+ content: 'Kalender',
+ placeholder: () => ({ year: 'JJJJ', month: 'MM', day: 'TT' }),
+ /** Name of the week column (its visible header is weekColumnShort) */
+ weekColumnHeader: 'Kalenderwoche',
+ weekColumnShort: 'KW',
+ weekNumberCell: (week: number) => `Kalenderwoche ${week}`,
+ presetTrigger: (value: string[]) => value.join(' – '),
+ },
+ /** Calendar extras: button bar, several chosen days, time zone */
+ calendar: {
+ today: 'Heute',
+ clear: 'Löschen',
+ /** Field of a DatePicker with selectionMode multiple */
+ noDates: 'Kein Datum gewählt',
+ datesSelected: { one: '{count} Tag ausgewählt', other: '{count} Tage ausgewählt' } as PluralForms,
+ timeZone: (name: string) => `Zeitzone: ${name}`,
+ },
+ quarterPicker: {
+ /** Text of a quarter in the field and on its button */
+ quarter: (quarter: number, year: number) => `Q${quarter} ${year}`,
+ /** Name of a quarter button: its text and the months */
+ quarterButton: (quarter: number, year: number, months: string) => `Q${quarter} ${year}, ${months}`,
+ trigger: 'Quartal wählen',
+ content: 'Quartale',
+ prevYear: 'Vorheriges Jahr',
+ nextYear: 'Nächstes Jahr',
+ clearTrigger: 'Quartal löschen',
+ placeholder: 'Q1 2026',
+ },
+ durationInput: {
+ clearTrigger: 'Dauer löschen',
+ days: 'Tage',
+ hours: 'Stunden',
+ minutes: 'Minuten',
+ seconds: 'Sekunden',
+ },
+ timeGrid: {
+ label: 'Freie Termine',
+ empty: 'Keine freien Termine',
+ loading: 'Termine werden geladen',
+ available: { one: '{count} freier Termin', other: '{count} freie Termine' } as PluralForms,
+ },
+ miniCalendar: {
+ label: 'Tage',
+ previous: 'Vorherige Tage',
+ next: 'Nächste Tage',
+ },
+ /** DateInput: placeholders of the parts and names of their spin buttons */
+ dateInput: {
+ placeholder: () => ({
+ day: 'TT',
+ month: 'MM',
+ year: 'JJJJ',
+ hour: 'hh',
+ minute: 'mm',
+ second: 'ss',
+ dayPeriod: '--',
+ era: '--',
+ }),
+ },
+ splitButton: { more: 'Weitere Aktionen' },
+ confirmPopup: { confirm: 'Ja', cancel: 'Abbrechen' },
+ passwordStrength: {
+ levels: ['Zu kurz', 'Schwach', 'Mittel', 'Gut', 'Stark'],
+ label: (level: string) => `Passwortstärke: ${level}`,
+ },
+ /** Field and text controls (Input, Textarea, PasswordInput, NumberInput …) */
+ field: { optional: 'optional', clear: 'Eingabe löschen' },
+ textarea: {
+ /** Visible counter */
+ counter: (length: number, max: number) => `${length} / ${max}`,
+ /** Announced after typing pauses */
+ remaining: (remaining: number, max: number) =>
+ remaining <= 0 ? `Höchstlänge von ${max} Zeichen erreicht` : `Noch ${remaining} von ${max} Zeichen`,
+ },
+ formErrorSummary: {
+ title: (count: number) =>
+ count === 1 ? 'Bitte korrigieren Sie 1 Angabe' : `Bitte korrigieren Sie ${count} Angaben`,
+ },
+ passwordRequirements: { label: 'Anforderungen an das Passwort', met: 'erfüllt', unmet: 'nicht erfüllt' },
+ dataView: {
+ layout: 'Ansicht',
+ list: 'Liste',
+ grid: 'Raster',
+ loadMore: 'Mehr laden',
+ /** Names the list of loading placeholders */
+ loading: 'Einträge werden geladen',
+ shown: (count: number, total: number) => `${count} von ${total} angezeigt`,
+ },
+ orderList: {
+ top: 'Ganz nach oben',
+ up: 'Nach oben',
+ down: 'Nach unten',
+ bottom: 'Ganz nach unten',
+ moved: (label: string, position: number, count: number) => `${label}: Position ${position} von ${count}`,
+ },
+ pickList: {
+ source: 'Verfügbar',
+ target: 'Ausgewählt',
+ add: 'Ausgewählte hinzufügen',
+ addAll: 'Alle hinzufügen',
+ remove: 'Ausgewählte entfernen',
+ removeAll: 'Alle entfernen',
+ moved: (count: number, target: string) =>
+ `${count === 1 ? '1 Eintrag' : `${count} Einträge`} nach „${target}“ verschoben`,
+ /** Search field of one list */
+ filter: (list: string) => `${list} durchsuchen`,
+ /** Drag and drop: where the dragged items go */
+ dropHint: (list: string) => `Hierher ziehen: ${list}`,
+ },
+ appShell: {
+ skipLink: 'Zum Inhalt springen',
+ openNavigation: 'Navigation öffnen',
+ navigation: 'Navigation',
+ collapseNavigation: 'Navigation einklappen',
+ expandNavigation: 'Navigation ausklappen',
+ resizeNavigation: 'Breite der Navigation ändern',
+ resizeAside: 'Breite der Seitenleiste ändern',
+ },
+ commandPalette: {
+ label: 'Befehle',
+ placeholder: 'Suchen oder Befehl eingeben …',
+ empty: 'Nichts gefunden',
+ navigate: 'auswählen',
+ run: 'ausführen',
+ close: 'schließen',
+ },
+ /** Option lists: Select, MultiSelect, Combobox, Listbox, TagsInput suggestions */
+ select: {
+ clear: 'Auswahl löschen',
+ filter: 'Optionen filtern',
+ selectAll: 'Alle auswählen',
+ removeItem: (label: string) => `${label} entfernen`,
+ removed: (label: string) => `${label} entfernt`,
+ maxReached: (max: number) => `Höchstens ${max} auswählbar`,
+ suggestions: 'Vorschläge',
+ },
+ /** Standalone Chip: name of the remove button */
+ chip: { remove: (label?: string): string => (label ? `${label} entfernen` : 'Entfernen') },
+ tagsInput: {
+ clearTriggerLabel: 'Alle entfernen',
+ deleteTagTriggerLabel: (value: string) => `${value} entfernen`,
+ tagAdded: (value: string) => `${value} hinzugefügt`,
+ tagsPasted: (values: string[]) => `${values.length} Einträge eingefügt`,
+ tagEdited: (value: string) => `${value} wird bearbeitet`,
+ tagUpdated: (value: string) => `${value} geändert`,
+ tagDeleted: (value: string) => `${value} entfernt`,
+ tagSelected: (value: string) => `${value} ausgewählt, Rücktaste zum Entfernen`,
+ },
+ navigationMenu: { rootLabel: 'Hauptnavigation' },
+ megaMenu: {
+ rootLabel: 'Hauptnavigation',
+ /** Menu button and drawer title on small screens */
+ menu: 'Menü',
+ /** Chevron button next to an entry that is a link itself */
+ submenu: (label: string) => `${label}: Untermenü`,
+ showAll: 'Alle anzeigen',
+ showAllOf: (title: string) => `Alle anzeigen: ${title}`,
+ /** Link of the featured area */
+ featuredLink: 'Mehr erfahren',
+ /** In the drawer: link to the page of an entry that also has a panel */
+ overview: (label: string) => `${label} – Übersicht`,
+ },
+ pageHeader: { back: 'Zurück' },
+ bottomNavigation: { label: 'Navigation' },
+ banner: { dismiss: 'Hinweis schließen' },
+ notificationCenter: {
+ label: 'Benachrichtigungen',
+ markAllRead: 'Alle als gelesen markieren',
+ markRead: 'Als gelesen markieren',
+ unread: 'Ungelesen',
+ unreadCount: (count: number | string) => `${count} ungelesen`,
+ empty: 'Keine Benachrichtigungen',
+ loadMore: 'Mehr laden',
+ older: 'Älter',
+ /** Polite announcement of new notifications */
+ announce: (count: number, title: string) =>
+ count === 1 ? `Neue Benachrichtigung: ${title}` : `${count} neue Benachrichtigungen`,
+ },
+ result: {
+ success: 'Erledigt',
+ info: 'Hinweis',
+ warn: 'Achtung',
+ error: 'Etwas ist schiefgelaufen',
+ '403': 'Kein Zugriff',
+ '404': 'Seite nicht gefunden',
+ '500': 'Serverfehler',
+ description403: 'Sie haben keine Berechtigung, diese Seite zu sehen.',
+ description404: 'Die Seite existiert nicht oder wurde verschoben.',
+ description500: 'Auf dem Server ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.',
+ },
+ tour: {
+ nextStep: 'Weiter',
+ prevStep: 'Zurück',
+ close: 'Tour beenden',
+ skip: 'Überspringen',
+ done: 'Fertig',
+ progressText: (details: { current: number; total: number }) => `${details.current + 1} von ${details.total}`,
+ },
+ tableOfContents: { title: 'Auf dieser Seite' },
+ spoiler: { show: 'Mehr anzeigen', hide: 'Weniger anzeigen' },
+ navigationProgress: { label: 'Seite wird geladen' },
+ copyButton: { label: 'Kopieren', copied: 'In die Zwischenablage kopiert' },
+ actionBar: { label: 'Aktionen für die Auswahl', clear: 'Auswahl aufheben' },
+ heatmap: { less: 'Weniger', more: 'Mehr', empty: 'kein Wert' },
+ scheduler: {
+ /** React Native: accessibility actions that move an event (±slot, ±day) */
+ shift: (minutes: number, days: number) =>
+ days
+ ? `${Math.abs(days) === 1 ? '1 Tag' : `${Math.abs(days)} Tage`} ${days > 0 ? 'später' : 'früher'}`
+ : `${Math.abs(minutes)} Minuten ${minutes > 0 ? 'später' : 'früher'}`,
+ label: 'Kalender',
+ today: 'Heute',
+ previous: 'Zurück',
+ next: 'Weiter',
+ view: 'Ansicht',
+ views: {
+ day: 'Tag',
+ week: 'Woche',
+ month: 'Monat',
+ agenda: 'Liste',
+ timeline: 'Zeitachse',
+ year: 'Jahr',
+ } as Record,
+ allDay: 'Ganztägig',
+ more: (count: number) => `+${count} weitere`,
+ newEvent: (date: string) => `Neuer Termin am ${date}`,
+ empty: 'Keine Termine',
+ changed: (title: string, when: string) => `${title}: ${when}`,
+ /** Resources */
+ resource: 'Ressource',
+ allResources: 'Alle Ressourcen',
+ previousResources: 'Vorherige Ressourcen',
+ nextResources: 'Nächste Ressourcen',
+ resourceRange: (from: number, to: number, count: number) => `Ressourcen ${from}–${to} von ${count}`,
+ resourceFilter: 'Ressource',
+ newEventFor: (resource: string, date: string) => `Neuer Termin für ${resource} am ${date}`,
+ /** Timeline */
+ range: 'Zeitraum',
+ ranges: { day: 'Tag', week: 'Woche', month: 'Monat' } as Record,
+ zoomIn: 'Vergrößern',
+ zoomOut: 'Verkleinern',
+ /** Series */
+ series: 'Serientermin',
+ seriesTitle: 'Serientermin ändern',
+ seriesQuestion: (title: string) => `„${title}“ gehört zu einer Serie. Was soll sich ändern?`,
+ onlyThis: 'Nur dieser Termin',
+ allInSeries: 'Alle Termine der Serie',
+ /** Rules for dropping */
+ rejected: (title: string, reason: string) => `${title} lässt sich hier nicht ablegen: ${reason}`,
+ notAllowed: 'nicht erlaubt',
+ overlaps: (title: string) => `überschneidet sich mit ${title}`,
+ /** Year view and "+N more" */
+ eventCount: { one: '{count} Termin', other: '{count} Termine' } as PluralForms,
+ closed: 'Außerhalb der Geschäftszeiten',
+ loading: 'Termine werden geladen',
+ loaded: { one: '{count} Termin geladen', other: '{count} Termine geladen' } as PluralForms,
+ error: 'Die Termine konnten nicht geladen werden.',
+ retry: 'Erneut versuchen',
+ },
+ recurrence: {
+ DAILY: { one: 'Täglich', other: 'Alle {count} Tage' } as PluralForms,
+ WEEKLY: { one: 'Wöchentlich', other: 'Alle {count} Wochen' } as PluralForms,
+ MONTHLY: { one: 'Monatlich', other: 'Alle {count} Monate' } as PluralForms,
+ YEARLY: { one: 'Jährlich', other: 'Alle {count} Jahre' } as PluralForms,
+ on: (what: string) => `am ${what}`,
+ /** "2. Montag", "letzten Freitag" */
+ nthWeekday: (nth: number, weekday: string) =>
+ nth === -1 ? `letzten ${weekday}` : nth < 0 ? `${-nth}.-letzten ${weekday}` : `${nth}. ${weekday}`,
+ /** Monthly by position: "Jeden 2. Montag im Monat" */
+ monthlyNth: (what: string) => `Jeden ${what} im Monat`,
+ /** Day of the month: "15.", "letzten Tag" */
+ monthDay: (day: number) => (day === -1 ? 'letzten Tag' : day < 0 ? `${-day}.-letzten Tag` : `${day}.`),
+ /** Yearly: "2. Montag im Mai", "24. Dezember" */
+ inMonth: (what: string, month: string) => `${what} im ${month}`,
+ dayOfMonth: (day: string, month: string) => `${day} ${month}`,
+ times: { one: 'einmal', other: '{count} Mal' } as PluralForms,
+ until: (date: string) => `bis ${date}`,
+ },
+ gantt: {
+ label: 'Projektplan',
+ task: 'Aufgabe',
+ start: 'Start',
+ end: 'Ende',
+ duration: 'Dauer',
+ days: { one: '{count} Tag', other: '{count} Tage' } as PluralForms,
+ milestone: 'Meilenstein',
+ group: 'Gruppe',
+ progress: (percent: number) => `${percent} % erledigt`,
+ dependsOn: (names: string) => `hängt ab von ${names}`,
+ critical: 'kritischer Pfad',
+ zoom: 'Zoom',
+ zooms: { day: 'Tag', week: 'Woche', month: 'Monat', quarter: 'Quartal' } as Record,
+ today: 'Heute',
+ week: (week: number) => `KW ${week}`,
+ quarter: (quarter: number) => `Q${quarter}`,
+ changed: (name: string, when: string) => `${name}: ${when}`,
+ progressChanged: (name: string, percent: number) => `${name}: ${percent} % erledigt`,
+ linked: (from: string, to: string) => `${to} hängt jetzt von ${from} ab`,
+ addDependency: 'Abhängigkeit hinzufügen…',
+ dependencyTitle: (name: string) => `Vorgänger von „${name}“`,
+ predecessor: 'Vorgänger',
+ noPredecessor: 'Keine Aufgabe möglich',
+ addDependencyConfirm: 'Hinzufügen',
+ empty: 'Keine Aufgaben',
+ },
+ codeHighlight: {
+ copy: 'Code kopieren',
+ showMore: 'Gesamten Code anzeigen',
+ showLess: 'Code einklappen',
+ wrap: 'Zeilen umbrechen',
+ files: 'Dateien',
+ added: 'hinzugefügt',
+ removed: 'entfernt',
+ },
+ pdfViewer: {
+ label: 'PDF-Dokument',
+ toolbar: 'PDF-Werkzeuge',
+ pages: 'Seiten des Dokuments',
+ sidebar: 'Seitenleiste',
+ thumbnails: 'Seiten',
+ outline: 'Inhalt',
+ noOutline: 'Kein Inhaltsverzeichnis',
+ pageLabel: (page: number) => `Seite ${page}`,
+ page: 'Seite',
+ ofPages: (count: number) => `von ${count}`,
+ pageStatus: (page: number, count: number) => `Seite ${page} von ${count}`,
+ previousPage: 'Vorherige Seite',
+ nextPage: 'Nächste Seite',
+ zoom: 'Zoom',
+ zoomIn: 'Vergrößern',
+ zoomOut: 'Verkleinern',
+ pageWidth: 'Seitenbreite',
+ pageFit: 'Ganze Seite',
+ rotate: 'Im Uhrzeigersinn drehen',
+ search: 'Suchen',
+ searchField: 'Im Dokument suchen',
+ matchCase: 'Groß-/Kleinschreibung beachten',
+ previousMatch: 'Vorheriger Treffer',
+ nextMatch: 'Nächster Treffer',
+ closeSearch: 'Suche schließen',
+ matches: (current: number, total: number) => `Treffer ${current} von ${total}`,
+ noMatches: 'Keine Treffer',
+ searching: 'Wird gesucht …',
+ wrappedTop: 'weiter am Ende',
+ wrappedBottom: 'weiter am Anfang',
+ download: 'Herunterladen',
+ print: 'Drucken',
+ printing: (page: number, count: number) => `Druck wird vorbereitet: Seite ${page} von ${count}`,
+ fullscreen: 'Vollbild',
+ loading: 'PDF wird geladen',
+ loaded: (count: number) => (count === 1 ? 'PDF geladen, 1 Seite' : `PDF geladen, ${count} Seiten`),
+ error: 'Das PDF konnte nicht geladen werden.',
+ invalid: 'Die Datei ist kein gültiges PDF oder beschädigt.',
+ missing: 'Das PDF wurde nicht gefunden.',
+ passwordTitle: 'Geschütztes PDF',
+ passwordDescription: 'Geben Sie das Passwort ein, um das Dokument zu öffnen.',
+ passwordLabel: 'Passwort',
+ passwordWrong: 'Das Passwort ist falsch. Bitte erneut versuchen.',
+ passwordSubmit: 'Öffnen',
+ passwordRequired: 'Ohne Passwort lässt sich das PDF nicht öffnen.',
+ fileName: 'dokument.pdf',
+ /** React Native: share or open the file; the web has no viewer for react-native-pdf */
+ share: 'Teilen',
+ unsupported: 'Die PDF-Ansicht gibt es nur in der App (iOS und Android).',
+ },
+ compare: {
+ label: 'Vorher-Nachher-Vergleich',
+ before: 'Vorher',
+ after: 'Nachher',
+ valueText: (percent: number) => `${percent} % vorher, ${100 - percent} % nachher`,
+ },
+ sankey: { source: 'Von', target: 'Nach', value: 'Wert' },
+ treemap: { share: (value: string, percent: string) => `${value} · ${percent}` },
+ countdown: {
+ days: (count: number): string => (count === 1 ? 'Tag' : 'Tage'),
+ hours: (count: number): string => (count === 1 ? 'Stunde' : 'Stunden'),
+ minutes: (count: number): string => (count === 1 ? 'Minute' : 'Minuten'),
+ seconds: (count: number): string => (count === 1 ? 'Sekunde' : 'Sekunden'),
+ },
+ rangePresets: {
+ thisWeek: 'Diese Woche',
+ lastWeek: 'Letzte Woche',
+ thisMonth: 'Dieser Monat',
+ lastMonth: 'Letzter Monat',
+ thisQuarter: 'Dieses Quartal',
+ lastQuarter: 'Letztes Quartal',
+ thisYear: 'Dieses Jahr',
+ lastYear: 'Letztes Jahr',
+ last3Days: 'Letzte 3 Tage',
+ last7Days: 'Letzte 7 Tage',
+ last14Days: 'Letzte 14 Tage',
+ last30Days: 'Letzte 30 Tage',
+ last90Days: 'Letzte 90 Tage',
+ last3Months: 'Letzte 3 Monate',
+ last6Months: 'Letzte 6 Monate',
+ last12Months: 'Letzte 12 Monate',
+ last4Quarters: 'Letzte 4 Quartale',
+ last3Years: 'Letzte 3 Jahre',
+ last5Years: 'Letzte 5 Jahre',
+ } as Record,
+ splitter: { resize: (panel: string) => `Größe von „${panel}“ ändern` },
+ image: {
+ preview: (alt: string) => `Vergrößert anzeigen: ${alt}`,
+ zoomIn: 'Vergrößern',
+ zoomOut: 'Verkleinern',
+ rotate: 'Drehen',
+ close: 'Schließen',
+ },
+ inplace: { edit: 'bearbeiten' },
+ scrollTop: { label: 'Nach oben' },
+ blockUi: { label: 'Wird geladen' },
+ speedDial: { label: 'Aktionen' },
+ stepper: {
+ prev: 'Zurück',
+ next: 'Weiter',
+ finish: 'Abschließen',
+ /** React Native: status of a step and the progress */
+ status: { done: 'abgeschlossen', active: 'aktueller Schritt', todo: 'offen' } as Record,
+ progress: (step: number, count: number) => `Schritt ${step} von ${count}`,
+ },
+ dataTable: {
+ row: (index: number) => `Zeile ${index + 1}`,
+ selectAll: 'Alle Zeilen dieser Seite auswählen',
+ selectRow: (row: string) => `${row} auswählen`,
+ subRows: (row: string) => `Unterzeilen von ${row}`,
+ search: (caption: string) => `${caption} durchsuchen`,
+ searchPlaceholder: 'Suchen …',
+ filter: (header: string) => `${header} filtern`,
+ filterAll: 'Alle',
+ columns: 'Spalten',
+ export: 'Exportieren (CSV)',
+ resize: (header: string) => `Breite von ${header}`,
+ edit: (header: string, value: string) => `${value || 'leer'}, ${header} bearbeiten`,
+ results: (count: number) => (count === 1 ? '1 Eintrag' : `${count} Einträge`),
+ group: (header: string, value: string, count: number) => `${header}: ${value}, ${count} Einträge`,
+ matchModes: {
+ startsWith: 'Beginnt mit',
+ contains: 'Enthält',
+ notContains: 'Enthält nicht',
+ endsWith: 'Endet mit',
+ equals: 'Ist gleich',
+ notEquals: 'Ist nicht gleich',
+ lt: 'Kleiner als',
+ lte: 'Kleiner oder gleich',
+ gt: 'Größer als',
+ gte: 'Größer oder gleich',
+ between: 'Zwischen',
+ in: 'Ist eins von',
+ dateIs: 'Am',
+ dateIsNot: 'Nicht am',
+ dateBefore: 'Vor',
+ dateAfter: 'Nach',
+ } as Record,
+ operator: 'Verknüpfung',
+ matchAll: 'Alle Bedingungen erfüllt',
+ matchAny: 'Eine Bedingung erfüllt',
+ matchMode: (index: number) => `Bedingung ${index + 1}`,
+ value: (index: number) => `Wert ${index + 1}`,
+ from: 'Von',
+ to: 'Bis',
+ addRule: 'Bedingung hinzufügen',
+ removeRule: (index: number) => `Bedingung ${index + 1} entfernen`,
+ apply: 'Anwenden',
+ clear: 'Zurücksetzen',
+ filterMenu: (header: string, active: boolean) =>
+ active ? `Filter für ${header}, aktiv` : `Filter für ${header}`,
+ yes: 'Ja',
+ no: 'Nein',
+ columnMenu: (header: string) => `Optionen für ${header}`,
+ sortAscending: 'Aufsteigend sortieren',
+ sortDescending: 'Absteigend sortieren',
+ clearSort: 'Sortierung entfernen',
+ /** React Native: sheets for sorting and the column filters */
+ sortBy: 'Sortieren',
+ filters: 'Filter',
+ sortPriority: (position: number, desc: boolean) =>
+ `${position}. Sortierung, ${desc ? 'absteigend' : 'aufsteigend'}`,
+ pinStart: 'Am Anfang fixieren',
+ pinEnd: 'Am Ende fixieren',
+ unpin: 'Nicht mehr fixieren',
+ moveEarlier: 'Eine Spalte nach vorne',
+ moveLater: 'Eine Spalte nach hinten',
+ hideColumn: 'Ausblenden',
+ groupBy: 'Nach dieser Spalte gruppieren',
+ ungroup: 'Gruppierung aufheben',
+ columnMoved: (header: string, position: number, count: number) =>
+ `${header} ist jetzt Spalte ${position} von ${count}`,
+ density: 'Zeilenhöhe',
+ densities: { sm: 'Kompakt', md: 'Normal', lg: 'Großzügig' },
+ fullscreen: 'Vollbild',
+ exitFullscreen: 'Vollbild beenden',
+ rowNumber: 'Nr.',
+ expandAll: 'Alle Zeilen aufklappen',
+ collapseAll: 'Alle Zeilen zuklappen',
+ details: (row: string) => `Details zu ${row}`,
+ actions: 'Aktionen',
+ rowActions: (row: string) => `Aktionen für ${row}`,
+ moveRow: (row: string) => `${row} verschieben`,
+ rowMoved: (row: string, position: number, count: number) =>
+ `${row} ist jetzt an Position ${position} von ${count}`,
+ editRow: (row: string) => `${row} bearbeiten`,
+ saveRow: (row: string) => `${row} speichern`,
+ cancelEdit: (row: string) => `Bearbeiten von ${row} abbrechen`,
+ rowsPerPage: 'Zeilen pro Seite',
+ exportAll: 'Alle Zeilen',
+ exportSelection: 'Ausgewählte Zeilen',
+ filterToggle: 'Filter anzeigen',
+ rowCount: (count: number) => (count === 1 ? '1 Zeile' : `${count} Zeilen`),
+ filteredCount: (count: number) => `${count} gefiltert`,
+ copy: (header: string, value: string) => `${value}, ${header} kopieren`,
+ selectAllRows: 'Alle Zeilen auswählen',
+ select: 'Auswahl',
+ reorder: 'Reihenfolge',
+ footer: 'Summenzeile',
+ aggregates: {
+ sum: (value: string) => `Summe ${value}`,
+ avg: (value: string) => `Ø ${value}`,
+ min: (value: string) => `Min. ${value}`,
+ max: (value: string) => `Max. ${value}`,
+ count: (value: string) => `Anzahl ${value}`,
+ } as Record string>,
+ exportMenu: 'Exportieren',
+ exportXlsx: 'Exportieren (Excel)',
+ exportAs: (format: 'csv' | 'xlsx', selection: boolean) =>
+ `${selection ? 'Ausgewählte Zeilen' : 'Alle Zeilen'} als ${format === 'xlsx' ? 'Excel' : 'CSV'}`,
+ cellsCopied: (count: number) => (count === 1 ? '1 Zelle kopiert' : `${count} Zellen kopiert`),
+ cellsPasted: (count: number, invalid: number) =>
+ `${count === 1 ? '1 Zelle' : `${count} Zellen`} eingefügt${invalid ? `, ${invalid} ungültig` : ''}`,
+ pasteErrors: {
+ number: 'Keine Zahl',
+ date: 'Kein Datum',
+ option: 'Keine der Auswahlmöglichkeiten',
+ boolean: 'Weder Ja noch Nein',
+ readonly: 'Nicht bearbeitbar',
+ } as Record,
+ changes: (count: number) => (count === 1 ? '1 Änderung' : `${count} Änderungen`),
+ changed: 'geändert',
+ saveChanges: 'Speichern',
+ discardChanges: 'Verwerfen',
+ changesSaved: (count: number) => (count === 1 ? '1 Änderung gespeichert' : `${count} Änderungen gespeichert`),
+ changesDiscarded: 'Änderungen verworfen',
+ undo: 'Rückgängig',
+ redo: 'Wiederholen',
+ undone: (count: number) => (count === 1 ? '1 Zelle zurückgesetzt' : `${count} Zellen zurückgesetzt`),
+ redone: (count: number) => (count === 1 ? '1 Zelle wiederhergestellt' : `${count} Zellen wiederhergestellt`),
+ rowMovedToGroup: (row: string, group: string, position: number, count: number) =>
+ `${row} ist jetzt in ${group}, Position ${position} von ${count}`,
+ rowMovedInTree: (row: string, parent: string | null, position: number, count: number) =>
+ `${row} verschoben${parent ? ` nach ${parent}` : ''}, Position ${position} von ${count}`,
+ find: 'In Tabelle suchen',
+ findMatches: (current: number, total: number) => (total ? `Treffer ${current} von ${total}` : 'Keine Treffer'),
+ findNext: 'Nächster Treffer',
+ findPrevious: 'Vorheriger Treffer',
+ findClose: 'Suche schließen',
+ total: 'Gesamt',
+ columnGroupEarlier: (header: string) => `${header} nach vorne`,
+ columnGroupLater: (header: string) => `${header} nach hinten`,
+ },
+ dataSource: {
+ loading: 'Wird geladen',
+ loaded: { one: '{count} Eintrag geladen', other: '{count} Einträge geladen' } as PluralForms,
+ empty: 'Keine Einträge gefunden',
+ error: 'Die Daten konnten nicht geladen werden.',
+ retry: 'Erneut versuchen',
+ minSearch: (count: number) => `Bitte mindestens ${count} Zeichen eingeben`,
+ /** Button at the end of a growing list */
+ loadMore: 'Mehr laden',
+ loadingMore: 'Weitere Einträge werden geladen',
+ },
+ dateRange: {
+ start: 'Beginn',
+ end: 'Ende',
+ clearTrigger: 'Zeitraum löschen',
+ },
+ timePicker: {
+ trigger: 'Uhrzeit wählen',
+ clearTrigger: 'Uhrzeit löschen',
+ /** TimeRangePicker */
+ clearRange: 'Uhrzeiten löschen',
+ },
+ dateSegments: {
+ day: 'Tag',
+ month: 'Monat',
+ year: 'Jahr',
+ hour: 'Stunde',
+ minute: 'Minute',
+ second: 'Sekunde',
+ dayPeriod: 'Tageszeit',
+ } as Record,
+ pinInput: { inputLabel: (index: number, length: number) => `Stelle ${index + 1} von ${length}` },
+ rating: { ratingValueText: (index: number) => (index === 1 ? '1 Stern' : `${index} Sterne`) },
+ carousel: {
+ nextTrigger: 'Nächstes Bild',
+ prevTrigger: 'Vorheriges Bild',
+ indicator: (index: number) => `Bild ${index + 1} zeigen`,
+ item: (index: number, count: number) => `Bild ${index + 1} von ${count}`,
+ autoplayStart: 'Automatisch abspielen',
+ autoplayStop: 'Abspielen anhalten',
+ progressText: (details: { page: number; totalPages: number }) => `${details.page + 1} / ${details.totalPages}`,
+ },
+ chart: {
+ showTable: 'Daten als Tabelle',
+ /** Header of the label column */
+ category: 'Kategorie',
+ sparkline: (label: string, first: string, last: string, min: string, max: string) =>
+ `${label}: von ${first} auf ${last}, Tiefstwert ${min}, Höchstwert ${max}`,
+ /** Column of a dataset without label */
+ series: (index: number) => `Reihe ${index + 1}`,
+ /** Waterfall: a step and the running sum */
+ change: 'Veränderung',
+ total: 'Summe',
+ /** Brush: the shown range */
+ brush: 'Ausschnitt',
+ brushThumbs: ['Beginn', 'Ende'],
+ legend: 'Legende',
+ share: 'Anteil',
+ value: 'Wert',
+ references: 'Referenzwerte',
+ /** Funnel: stage and conversion */
+ stage: 'Schritt',
+ conversion: 'Umwandlung',
+ fromPrevious: (percent: string) => `${percent} vom vorherigen Schritt`,
+ /** Bullet chart: value, maximum and target */
+ target: 'Ziel',
+ bulletValue: (value: string, max: string, target?: string) =>
+ target ? `${value} von ${max}, Ziel ${target}` : `${value} von ${max}`,
+ },
+ editor: {
+ toolbar: 'Formatierung',
+ bold: 'Fett',
+ italic: 'Kursiv',
+ underline: 'Unterstrichen',
+ strike: 'Durchgestrichen',
+ heading2: 'Überschrift',
+ heading3: 'Zwischenüberschrift',
+ bulletList: 'Aufzählung',
+ orderedList: 'Nummerierte Liste',
+ blockquote: 'Zitat',
+ code: 'Code',
+ link: 'Link',
+ undo: 'Rückgängig',
+ redo: 'Wiederholen',
+ linkUrl: 'Linkadresse',
+ linkApply: 'Übernehmen',
+ linkRemove: 'Link entfernen',
+ linkCancel: 'Abbrechen',
+ taskList: 'Aufgabenliste',
+ codeBlock: 'Codeblock',
+ horizontalRule: 'Trennlinie',
+ image: 'Bild',
+ imageUrl: 'Bildadresse',
+ imageAlt: 'Alternativtext',
+ imageDecorative: 'Nur Schmuck (ohne Alternativtext)',
+ imageUpload: 'Datei wählen',
+ imageInsert: 'Einfügen',
+ imageUpdate: 'Übernehmen',
+ table: 'Tabelle',
+ tableBar: 'Tabelle bearbeiten',
+ tableActions: {
+ insertTable: 'Tabelle einfügen',
+ addRowBefore: 'Zeile darüber',
+ addRowAfter: 'Zeile darunter',
+ addColumnBefore: 'Spalte davor',
+ addColumnAfter: 'Spalte danach',
+ deleteRow: 'Zeile löschen',
+ deleteColumn: 'Spalte löschen',
+ toggleHeaderRow: 'Kopfzeile ein/aus',
+ mergeOrSplit: 'Zellen verbinden/teilen',
+ deleteTable: 'Tabelle löschen',
+ } as Record,
+ taskItem: (text: string, checked: boolean) => `${text || 'Leere Aufgabe'}, ${checked ? 'erledigt' : 'offen'}`,
+ shortcut: (letter: string, shift: boolean) => `Strg+${shift ? 'Umschalt+' : ''}${letter}`,
+ alignLeft: 'Linksbündig',
+ alignCenter: 'Zentriert',
+ alignRight: 'Rechtsbündig',
+ alignJustify: 'Blocksatz',
+ textColor: 'Textfarbe',
+ backgroundColor: 'Hintergrundfarbe',
+ colorDefault: 'Standard',
+ colors: {
+ primary: 'Akzent',
+ info: 'Info',
+ success: 'Erfolg',
+ warn: 'Warnung',
+ danger: 'Fehler',
+ muted: 'Gedämpft',
+ } as Record,
+ subscript: 'Tiefgestellt',
+ superscript: 'Hochgestellt',
+ source: 'HTML-Quelltext',
+ sourceField: 'HTML-Quelltext bearbeiten',
+ details: 'Aufklappbarer Abschnitt',
+ detailsToggle: (open: boolean): string => (open ? 'Abschnitt zuklappen' : 'Abschnitt aufklappen'),
+ invisibleCharacters: 'Unsichtbare Zeichen',
+ tableOfContents: 'Inhaltsverzeichnis',
+ characters: (count: number, limit?: number) =>
+ limit === undefined ? `${count} Zeichen` : `${count} von ${limit} Zeichen`,
+ words: (count: number) => (count === 1 ? '1 Wort' : `${count} Wörter`),
+ limitReached: 'Höchstlänge erreicht',
+ imageUploading: 'Bild wird hochgeladen …',
+ imageUploaded: 'Bild eingefügt',
+ imageFailed: 'Das Bild konnte nicht hochgeladen werden',
+ bubbleMenu: 'Auswahl formatieren',
+ floatingMenu: 'Block einfügen',
+ menuHint: 'Alt+F10: Menü',
+ suggestions: 'Vorschläge',
+ noSuggestions: 'Keine Treffer',
+ suggestionCount: (count: number) => (count === 1 ? '1 Vorschlag' : `${count} Vorschläge`),
+ mentioned: (label: string) => `${label} erwähnt`,
+ },
+ gallery: {
+ fullscreen: 'Vollbild',
+ close: 'Vollbild schließen',
+ thumbnails: 'Vorschaubilder',
+ thumbnail: (index: number, count: number) => `Bild ${index + 1} von ${count} zeigen`,
+ /** Lightbox toolbar (fullscreen view) */
+ toolbar: 'Bildwerkzeuge',
+ zoomIn: 'Vergrößern',
+ zoomOut: 'Verkleinern',
+ resetZoom: 'Originalgröße',
+ rotate: 'Drehen',
+ download: 'Herunterladen',
+ screen: 'Bildschirmfüllend',
+ exitScreen: 'Bildschirmfüllend beenden',
+ zoom: (percent: number) => `Zoom ${percent} %`,
+ /** Keyboard help of the lightbox */
+ shortcuts: 'Pfeiltasten: Bild wechseln · + und −: Zoom · 0: Originalgröße · R: Drehen · F: Bildschirmfüllend',
+ video: 'Video',
+ },
+ /** Ark names the colour picker parts in English and has no translations for them */
+ colorPicker: {
+ trigger: (color: string) => `Farbe wählen, aktuell ${color}`,
+ /** Name of the trigger without a Field or aria-label */
+ name: 'Farbe',
+ area: 'Sättigung und Helligkeit',
+ hue: 'Farbton',
+ alpha: 'Deckkraft',
+ hex: 'Hex-Wert',
+ swatch: (color: string) => `${color} wählen`,
+ /** ColorInput: the swatch button that opens the picker */
+ open: 'Farbwähler öffnen',
+ eyeDropper: 'Farbe vom Bildschirm aufnehmen',
+ format: 'Format',
+ formats: { rgb: 'RGB', hsl: 'HSL', hsb: 'HSB', oklch: 'OKLCH' } as Record,
+ /** Names of the channel fields per format */
+ channels: {
+ rgb: ['Rot', 'Grün', 'Blau'],
+ hsl: ['Farbton', 'Sättigung', 'Helligkeit'],
+ hsb: ['Farbton', 'Sättigung', 'Hellwert'],
+ oklch: ['Helligkeit', 'Buntheit', 'Farbton'],
+ } as Record,
+ palette: 'Farben',
+ },
+ tree: {
+ treeLabel: 'Baum',
+ moved: (label: string, parent: string | null, position: number, count: number) =>
+ `${label} verschoben${parent ? ` nach ${parent}` : ''}, Position ${position} von ${count}`,
+ /** Row below a branch while its children load */
+ loadingRow: 'Wird geladen …',
+ loadingNode: (label: string) => `${label}: Wird geladen`,
+ loadedNode: {
+ one: '{label}: {count} Eintrag geladen',
+ other: '{label}: {count} Einträge geladen',
+ } as PluralForms,
+ loadError: (label: string) => `${label} konnte nicht geladen werden`,
+ /** Row below a branch whose children failed to load; activating it loads again */
+ retry: 'Laden fehlgeschlagen – erneut versuchen',
+ filter: 'Baum durchsuchen',
+ matches: { one: '{count} Treffer', other: '{count} Treffer' } as PluralForms,
+ noMatches: 'Keine Treffer',
+ expandAll: 'Alle aufklappen',
+ collapseAll: 'Alle zuklappen',
+ cut: (label: string) => `${label} ausgeschnitten – mit Strg+V an einem anderen Knoten einfügen`,
+ cutCancelled: 'Ausschneiden abgebrochen',
+ dropDenied: 'Hier nicht möglich',
+ },
+ treeSelect: {
+ search: 'Suchen',
+ /** Chip for the selected nodes beyond `maxShown` */
+ more: (count: number) => `+${count}`,
+ moreLabel: (count: number) => `${count} weitere ausgewählt`,
+ },
+ cascadeSelect: {
+ search: 'Suchen',
+ /** Button next to the search field that opens the levels */
+ showLevels: 'Ebenen anzeigen',
+ /** First entry of a level with `changeOnSelect`: chooses the level itself */
+ choose: (label: string) => `${label} wählen`,
+ /** A hit of the search: the whole path */
+ path: (labels: string[]) => labels.join(' › '),
+ },
+ timeline: {
+ loadMore: 'Ältere Einträge laden',
+ loading: 'Ältere Einträge werden geladen',
+ },
+ orgChart: {
+ expand: (label: string) => `Team von ${label} anzeigen`,
+ collapse: (label: string) => `Team von ${label} ausblenden`,
+ loading: (label: string) => `${label}: Wird geladen`,
+ retry: (label: string) => `${label}: Laden fehlgeschlagen – erneut versuchen`,
+ },
+ fileUpload: {
+ dropzone: 'Dateien ablegen',
+ itemPreview: (file: File) => `Vorschau von ${file.name}`,
+ deleteFile: (file: File) => `${file.name} entfernen`,
+ dropzoneText: (multiple: boolean): string =>
+ multiple ? 'Dateien hierher ziehen oder' : 'Datei hierher ziehen oder',
+ trigger: (multiple: boolean): string => (multiple ? 'Dateien auswählen' : 'Datei auswählen'),
+ /** Messages for rejected files (Ark FileUpload error codes) */
+ errors: {
+ TOO_MANY_FILES: 'Zu viele Dateien',
+ FILE_INVALID_TYPE: 'Dateityp nicht erlaubt',
+ FILE_TOO_LARGE: 'Datei ist zu groß',
+ FILE_TOO_SMALL: 'Datei ist zu klein',
+ FILE_INVALID: 'Datei ist ungültig',
+ FILE_EXISTS: 'Datei ist schon ausgewählt',
+ } as Record,
+ upload: 'Hochladen',
+ uploadAll: 'Alle hochladen',
+ cancel: (file: File) => `Hochladen von ${file.name} abbrechen`,
+ retry: (file: File) => `${file.name} erneut hochladen`,
+ progress: (file: File) => `Fortschritt von ${file.name}`,
+ status: {
+ pending: 'Bereit zum Hochladen',
+ uploading: (percent: number) => `Wird hochgeladen … ${percent} %`,
+ done: 'Hochgeladen',
+ error: 'Hochladen fehlgeschlagen',
+ cancelled: 'Abgebrochen',
+ },
+ /** Announced when an upload ends */
+ uploaded: (file: File) => `${file.name} hochgeladen`,
+ failed: (file: File) => `${file.name} konnte nicht hochgeladen werden`,
+ dropAccept: (multiple: boolean): string =>
+ multiple ? 'Loslassen, um die Dateien hinzuzufügen' : 'Loslassen, um die Datei hinzuzufügen',
+ dropReject: 'Diese Dateien sind hier nicht erlaubt',
+ fullscreen: 'Dateien irgendwo ablegen',
+ /** React Native: where FileUpload takes files from (the pickers of the app) */
+ sources: { documents: 'Dokument wählen', photos: 'Aus Fotos wählen', camera: 'Foto aufnehmen' },
+ pasted: (count: number) => (count === 1 ? '1 Datei eingefügt' : `${count} Dateien eingefügt`),
+ },
+ statistic: { loading: 'Wird geladen' },
+ truncate: { more: 'Mehr anzeigen', less: 'Weniger anzeigen' },
+ overflowList: {
+ /** Hidden part of the "+N" button's name (after the visible "+N") */
+ more: 'weitere',
+ title: 'Weitere Einträge',
+ },
+ toggletip: {
+ label: 'Weitere Informationen',
+ labelFor: (label: string) => `Weitere Informationen zu ${label}`,
+ },
+ checkboxGroup: { maxReached: (max: number) => `Höchstens ${max} auswählbar` },
+ skipLinks: { label: 'Sprunglinks' },
+ hotkeys: {
+ /** Key names for the display (Kbd `hotkey`, HotkeysDialog); Apple devices show symbols */
+ keys: {
+ ctrl: 'Strg',
+ alt: 'Alt',
+ shift: 'Umschalt',
+ meta: 'Win',
+ space: 'Leertaste',
+ enter: 'Eingabe',
+ escape: 'Esc',
+ backspace: 'Rücktaste',
+ delete: 'Entf',
+ tab: 'Tab',
+ },
+ /** Between the steps of a sequence ("G dann I") */
+ then: 'dann',
+ title: 'Tastenkürzel',
+ description: 'Alle Tastenkürzel dieser Seite',
+ empty: 'Keine Tastenkürzel',
+ /** Description of the "?" shortcut */
+ open: 'Tastenkürzel anzeigen',
+ /** Group of shortcuts without their own */
+ general: 'Allgemein',
+ /** Description of the CommandPalette's shortcut */
+ commandPalette: 'Befehle öffnen',
+ },
+ persona: {
+ presence: { online: 'Online', away: 'Abwesend', busy: 'Beschäftigt', offline: 'Offline' } as Record<
+ string,
+ string
+ >,
+ },
+ inlineEdit: {
+ edit: 'bearbeiten',
+ save: 'Speichern',
+ cancel: 'Abbrechen',
+ empty: 'Leer',
+ saving: 'Wird gespeichert …',
+ /** Title of the popover */
+ title: (label: string) => `${label} bearbeiten`,
+ },
+ searchField: { label: 'Suche', placeholder: 'Suchen …', clear: 'Suche löschen', scope: 'Suchbereich' },
+ filterPanel: {
+ label: 'Filter',
+ toggle: 'Filter',
+ /** Hidden part of the toggle's name after the visible count */
+ active: 'aktiv',
+ status: (count: number) =>
+ count === 0 ? 'Keine Filter aktiv' : count === 1 ? '1 Filter aktiv' : `${count} Filter aktiv`,
+ },
+ filterChips: {
+ label: 'Aktive Filter',
+ remove: (label: string) => `Filter ${label} entfernen`,
+ reset: 'Alle zurücksetzen',
+ },
+ conditionBuilder: {
+ label: 'Bedingungen',
+ combinator: 'Verknüpfung',
+ and: 'Und',
+ or: 'Oder',
+ all: 'Alle Bedingungen erfüllt',
+ any: 'Eine Bedingung erfüllt',
+ not: 'Nicht',
+ group: (path: string) => `Gruppe ${path}`,
+ rule: (path: string) => `Bedingung ${path}`,
+ addRule: 'Bedingung hinzufügen',
+ addGroup: 'Gruppe hinzufügen',
+ removeRule: (path: string) => `Bedingung ${path} entfernen`,
+ removeGroup: (path: string) => `Gruppe ${path} entfernen`,
+ field: 'Feld',
+ operator: 'Operator',
+ value: 'Wert',
+ from: 'Von',
+ to: 'Bis',
+ yes: 'Ja',
+ no: 'Nein',
+ empty: 'Keine Bedingungen – alle Einträge passen',
+ /** Operators beyond the DataTable match modes */
+ operators: { empty: 'Ist leer', notEmpty: 'Ist nicht leer', in: 'Ist eins von' } as Record,
+ added: 'Bedingung hinzugefügt',
+ groupAdded: 'Gruppe hinzugefügt',
+ removed: 'Bedingung entfernt',
+ groupRemoved: 'Gruppe entfernt',
+ /** The DataTable's toggle */
+ advanced: 'Erweiterter Filter',
+ },
+ formList: {
+ add: 'Eintrag hinzufügen',
+ item: (position: number) => `Eintrag ${position}`,
+ remove: (item: string) => `${item} entfernen`,
+ moveUp: (item: string) => `${item} nach oben`,
+ moveDown: (item: string) => `${item} nach unten`,
+ added: (item: string) => `${item} hinzugefügt`,
+ removed: (item: string) => `${item} entfernt`,
+ moved: (item: string, position: number) => `${item} an Position ${position} verschoben`,
+ max: (max: number) => `Höchstens ${max} Einträge`,
+ min: (min: number) => `Mindestens ${min} Einträge`,
+ },
+ keyValueEditor: {
+ key: 'Schlüssel',
+ value: 'Wert',
+ add: 'Zeile hinzufügen',
+ item: (position: number) => `Zeile ${position}`,
+ duplicate: 'Schlüssel kommt mehrfach vor',
+ },
+ wizard: {
+ summary: 'Zusammenfassung',
+ validating: 'Wird geprüft …',
+ invalid: 'Bitte die Angaben in diesem Schritt prüfen',
+ },
+ aiLabel: {
+ label: 'KI-generiert',
+ explanation:
+ 'Dieser Inhalt wurde mit künstlicher Intelligenz erstellt. Bitte prüfen Sie ihn vor der Verwendung.',
+ help: 'Was bedeutet KI-generiert?',
+ },
+ qrCode: {
+ label: (value: string) => `QR-Code: ${value}`,
+ downloadPng: 'Als PNG herunterladen',
+ downloadSvg: 'Als SVG herunterladen',
+ },
+ signaturePad: {
+ label: 'Unterschrift',
+ roleDescription: 'Unterschriftsfeld',
+ control: 'Unterschriftsfeld – mit Maus, Stift oder Finger unterschreiben',
+ placeholder: 'Hier unterschreiben',
+ clear: 'Löschen',
+ undo: 'Rückgängig',
+ type: 'Unterschrift eintippen',
+ draw: 'Unterschrift zeichnen',
+ typedLabel: 'Name als Unterschrift',
+ cleared: 'Unterschrift gelöscht',
+ },
+ imageCropper: {
+ label: 'Bild zuschneiden',
+ roleDescription: 'Bildzuschnitt',
+ loading: 'Bild wird geladen',
+ description: (zoom: number | null, rotation: number | null) =>
+ `Zoom ${Math.round((zoom ?? 1) * 100)} %, gedreht um ${Math.round(rotation ?? 0)} Grad`,
+ selection: (circle: boolean): string => (circle ? 'Kreisförmiger Ausschnitt' : 'Ausschnitt'),
+ selectionRoleDescription: 'verschiebbarer Ausschnitt',
+ instructions:
+ 'Pfeiltasten verschieben den Ausschnitt, Alt und Pfeiltasten ändern seine Größe, Plus und Minus zoomen.',
+ selectionValue: (width: number, height: number, x: number, y: number) =>
+ `${width} × ${height} Pixel, Position ${x}, ${y}`,
+ zoomIn: 'Vergrößern',
+ zoomOut: 'Verkleinern',
+ rotateLeft: 'Nach links drehen',
+ rotateRight: 'Nach rechts drehen',
+ flip: 'Horizontal spiegeln',
+ reset: 'Zurücksetzen',
+ toolbar: 'Bild bearbeiten',
+ },
+ floatingPanel: {
+ minimize: 'Minimieren',
+ maximize: 'Maximieren',
+ restore: 'Wiederherstellen',
+ close: 'Schließen',
+ hint: 'Alt und Pfeiltasten verschieben das Fenster, Alt, Umschalt und Pfeiltasten ändern seine Größe.',
+ },
+ jsonViewer: {
+ label: 'JSON-Daten',
+ search: 'Durchsuchen',
+ copy: (path: string) => `${path} kopieren`,
+ copied: (path: string) => `${path} kopiert`,
+ items: (count: number) => (count === 1 ? '1 Eintrag' : `${count} Einträge`),
+ keys: (count: number) => (count === 1 ? '1 Schlüssel' : `${count} Schlüssel`),
+ matches: (count: number) => (count === 0 ? 'Keine Treffer' : count === 1 ? '1 Treffer' : `${count} Treffer`),
+ types: {
+ object: 'Objekt',
+ array: 'Liste',
+ string: 'Text',
+ number: 'Zahl',
+ boolean: 'Wahrheitswert',
+ null: 'leer',
+ undefined: 'undefiniert',
+ } as Record,
+ },
+ jsonInput: {
+ format: 'Formatieren',
+ valid: 'Gültiges JSON',
+ invalid: 'Ungültiges JSON',
+ invalidAt: (line: number, column: number) => `Ungültiges JSON in Zeile ${line}, Spalte ${column}`,
+ },
+ downloadTrigger: { label: 'Herunterladen' },
+ marquee: { label: 'Laufband', pause: 'Laufband anhalten', play: 'Laufband fortsetzen' },
+ angleSlider: { label: 'Winkel', value: (degrees: number) => `${degrees} Grad` },
+ mentions: {
+ label: 'Nachricht',
+ suggestions: 'Vorschläge',
+ /** Announced when the suggestions open or change */
+ count: (count: number) =>
+ count === 0 ? 'Keine Vorschläge' : count === 1 ? '1 Vorschlag' : `${count} Vorschläge`,
+ hint: (triggers: string) => `${triggers} öffnet Vorschläge, Pfeiltasten wählen, Eingabe übernimmt`,
+ },
+ phoneInput: {
+ country: 'Land',
+ number: 'Telefonnummer',
+ search: 'Land suchen',
+ },
+ colorSwatchPicker: { label: 'Farbe' },
+ emojiPicker: {
+ /**
+ * Names and search words of the emojis in this language, one per line: "😀 Name|search words" – empty keeps the
+ * English names; the English words always find, too
+ */
+ emojis: `😀 Grinsendes Gesicht|lachen freude fröhlich
+😃 Grinsen mit großen Augen|lachen freude
+😄 Lachendes Gesicht|lachen freude glücklich
+😁 Strahlendes Gesicht|grinsen zähne
+😆 Lachen mit zugekniffenen Augen|lachen lustig
+😅 Lachen mit Schweißtropfen|erleichtert puh
+🤣 Vor Lachen auf dem Boden|lachen lol lustig
+😂 Tränen vor Lachen|lachen lustig weinen
+🙂 Leicht lächelndes Gesicht|lächeln
+🙃 Umgekehrtes Gesicht|ironie albern
+😉 Zwinkerndes Gesicht|zwinkern scherz
+😊 Lächeln mit lachenden Augen|freundlich erröten
+😇 Lächeln mit Heiligenschein|engel unschuldig
+🥰 Lächeln mit Herzen|verliebt liebe
+😍 Herzaugen|verliebt liebe toll
+🤩 Sternaugen|begeistert wow
+😘 Kuss zuwerfen|kuss liebe
+😋 Lecker|genießen essen zunge
+😛 Zunge herausstrecken|frech
+😜 Zwinkern mit Zunge|albern frech
+🤪 Verrücktes Gesicht|albern verrückt
+🤑 Geldgesicht|geld reich
+🤗 Umarmung|umarmen danke
+🤭 Hand vor dem Mund|kichern hoppla
+🤫 Pst|leise geheim ruhe
+🤔 Nachdenkliches Gesicht|denken überlegen hmm
+🤐 Reißverschlussmund|schweigen geheim
+🤨 Hochgezogene Augenbraue|skeptisch zweifel
+😐 Neutrales Gesicht|neutral egal
+😑 Ausdrucksloses Gesicht|genervt egal
+😶 Gesicht ohne Mund|sprachlos
+😏 Süffisantes Lächeln|grinsen schelmisch
+😒 Gelangweiltes Gesicht|unzufrieden genervt
+🙄 Augenrollen|genervt
+😬 Grimasse|peinlich angespannt
+😌 Erleichtertes Gesicht|zufrieden ruhig
+😔 Nachdenklich traurig|traurig
+😪 Müdes Gesicht|schläfrig
+😴 Schlafendes Gesicht|schlafen müde zzz
+😷 Gesicht mit Maske|krank maske
+🤒 Gesicht mit Thermometer|krank fieber
+🤢 Übelkeit|krank übel
+🤧 Niesendes Gesicht|schnupfen erkältung
+🥵 Heißes Gesicht|hitze schwitzen
+🥶 Kaltes Gesicht|kalt frieren
+😵 Benommenes Gesicht|schwindelig
+🤯 Explodierender Kopf|überwältigt wow
+🤠 Cowboy|hut
+🥳 Partygesicht|feiern party geburtstag
+😎 Sonnenbrille|cool lässig
+🤓 Strebergesicht|nerd brille
+🧐 Gesicht mit Monokel|prüfen genau
+😕 Verwirrtes Gesicht|unsicher
+😟 Besorgtes Gesicht|sorge
+🙁 Leicht trauriges Gesicht|traurig
+😮 Offener Mund|überrascht staunen
+😲 Erstauntes Gesicht|überrascht schock
+😳 Errötetes Gesicht|peinlich verlegen
+🥺 Flehendes Gesicht|bitte bettelnd
+😨 Ängstliches Gesicht|angst
+😰 Besorgt mit Schweiß|angst stress
+😢 Weinendes Gesicht|traurig träne
+😭 Laut weinend|traurig weinen
+😱 Schreiendes Gesicht|angst schock horror
+😖 Verwirrtes Gesicht|frustriert
+😞 Enttäuschtes Gesicht|enttäuscht traurig
+😓 Niedergeschlagen mit Schweiß|stress
+😩 Erschöpftes Gesicht|müde fertig
+😫 Müde|erschöpft
+🥱 Gähnendes Gesicht|gähnen müde langweilig
+😤 Triumphierendes Gesicht|schnauben stolz
+😡 Wütendes Gesicht|wut ärger
+😠 Verärgertes Gesicht|ärger böse
+🤬 Fluchendes Gesicht|schimpfen wut
+😈 Lachender Teufel|teufel frech
+💀 Totenkopf|tot schädel
+💩 Kothaufen|lustig
+🤡 Clown|clownsgesicht
+👻 Gespenst|geist halloween
+👽 Außerirdischer|alien ufo
+🤖 Roboter|bot maschine ki
+😺 Lachende Katze|katze
+😻 Katze mit Herzaugen|katze verliebt
+🙈 Nichts sehen|affe peinlich
+🙉 Nichts hören|affe
+🙊 Nichts sagen|affe geheim
+👋 Winkende Hand|hallo tschüss winken
+🤚 Erhobene Hand|stopp melden
+✋ Hand|stopp halt fünf
+🖖 Vulkanischer Gruß|spock
+👌 OK-Zeichen|ok gut perfekt
+🤌 Zusammengelegte Finger|italienisch was
+✌️ Victory-Zeichen|sieg frieden
+🤞 Gekreuzte Finger|glück daumen drücken
+🤟 Ich liebe dich|liebe geste
+🤘 Teufelsgruß|rock metal
+🤙 Ruf mich an|telefon
+👈 Zeigefinger links|links zeigen
+👉 Zeigefinger rechts|rechts zeigen
+👆 Zeigefinger nach oben|oben
+👇 Zeigefinger nach unten|unten
+☝️ Zeigefinger|eins hinweis
+👍 Daumen hoch|gut ja zustimmung like
+👎 Daumen runter|schlecht nein ablehnung
+✊ Erhobene Faust|faust solidarität
+👊 Faust|faustschlag
+👏 Klatschende Hände|applaus bravo klatschen
+🙌 Erhobene Hände|feiern hurra
+👐 Offene Hände|umarmung
+🤲 Handflächen nach oben|bitten
+🤝 Handschlag|vereinbarung deal partner
+🙏 Gefaltete Hände|bitte danke beten
+✍️ Schreibende Hand|schreiben notiz
+💪 Bizeps|stark kraft muskel
+🧠 Gehirn|denken klug
+👀 Augen|schauen sehen
+👁️ Auge|sehen
+👄 Mund|lippen
+👶 Baby|kind säugling
+🧒 Kind|kind
+👦 Junge|kind
+👧 Mädchen|kind
+🧑 Person|mensch
+👨 Mann|mensch
+👩 Frau|mensch
+🧓 Ältere Person|alt senior
+👮 Polizist|polizei
+🕵️ Detektiv|spion ermittler
+👷 Bauarbeiter|baustelle helm
+🧑💻 Person am Computer|programmieren entwickler technik
+🧑🏫 Lehrkraft|lehrer schule
+🧑⚕️ Ärztin oder Arzt|medizin gesundheit
+🧑🍳 Koch|kochen küche
+🦸 Superheld|held
+🙋 Person mit erhobener Hand|melden frage hallo
+🤷 Schulterzucken|egal weiß nicht
+🤦 Hand vor die Stirn|facepalm oh nein
+🙇 Verbeugung|entschuldigung danke
+💁 Infoschalter-Person|info hilfe
+🏃 Laufende Person|laufen rennen sport
+🚶 Gehende Person|gehen spazieren
+💃 Tanzende Frau|tanzen party
+🕺 Tanzender Mann|tanzen party
+👨👩👧 Familie|eltern kind
+👥 Silhouetten|gruppe team leute
+🗣️ Sprechender Kopf|sprechen reden
+🐶 Hundegesicht|hund haustier
+🐱 Katzengesicht|katze haustier
+🐭 Mausgesicht|maus
+🐹 Hamster|haustier
+🐰 Hasengesicht|hase kaninchen ostern
+🦊 Fuchs|fuchs
+🐻 Bär|bär
+🐼 Panda|bär
+🐨 Koala|bär
+🐯 Tigergesicht|tiger
+🦁 Löwe|löwe
+🐮 Kuhgesicht|kuh
+🐷 Schweinegesicht|schwein
+🐸 Frosch|frosch
+🐵 Affengesicht|affe
+🐔 Huhn|huhn vogel
+🐧 Pinguin|vogel
+🐦 Vogel|vogel
+🦆 Ente|vogel
+🦅 Adler|vogel
+🦉 Eule|vogel nacht
+🐝 Biene|honig insekt
+🐛 Raupe|insekt
+🦋 Schmetterling|insekt
+🐌 Schnecke|langsam
+🐞 Marienkäfer|glück insekt
+🐢 Schildkröte|langsam
+🐍 Schlange|reptil
+🐙 Oktopus|krake meer
+🐠 Tropischer Fisch|fisch meer
+🐬 Delfin|meer
+🐳 Wal|meer
+🦈 Hai|meer
+🐊 Krokodil|reptil
+🦒 Giraffe|tier
+🐘 Elefant|tier
+🐴 Pferdegesicht|pferd
+🦄 Einhorn|fantasie
+🐾 Pfotenabdrücke|tier spuren
+💐 Blumenstrauß|blumen geschenk
+🌸 Kirschblüte|blume frühling
+🌹 Rose|blume liebe
+🌻 Sonnenblume|blume sommer
+🌷 Tulpe|blume frühling
+🌱 Keimling|pflanze wachsen
+🌲 Nadelbaum|baum wald
+🌳 Laubbaum|baum
+🌴 Palme|urlaub strand
+🌵 Kaktus|wüste pflanze
+🍀 Kleeblatt|glück
+🍁 Ahornblatt|herbst
+🍂 Herbstlaub|herbst blätter
+🌍 Weltkugel Europa und Afrika|erde welt global
+🌙 Mondsichel|mond nacht
+⭐ Stern|stern
+🌟 Leuchtender Stern|stern glanz
+⚡ Blitz|strom energie schnell
+🔥 Feuer|heiß flamme trend
+🌈 Regenbogen|farben
+☀️ Sonne|sonnig wetter
+⛅ Sonne hinter Wolke|wetter wolkig
+☁️ Wolke|wetter
+🌧️ Regenwolke|regen wetter
+❄️ Schneeflocke|schnee winter kalt
+💧 Tropfen|wasser
+🌊 Welle|meer wasser
+🍏 Grüner Apfel|obst
+🍎 Roter Apfel|obst
+🍐 Birne|obst
+🍊 Mandarine|obst orange
+🍋 Zitrone|obst sauer
+🍌 Banane|obst
+🍉 Wassermelone|obst sommer
+🍇 Trauben|obst wein
+🍓 Erdbeere|obst
+🍒 Kirschen|obst
+🍑 Pfirsich|obst
+🥭 Mango|obst
+🍍 Ananas|obst
+🥥 Kokosnuss|obst
+🥝 Kiwi|obst
+🍅 Tomate|gemüse
+🥑 Avocado|gemüse
+🥦 Brokkoli|gemüse
+🥕 Karotte|möhre gemüse
+🌽 Maiskolben|mais gemüse
+🌶️ Peperoni|scharf
+🥔 Kartoffel|gemüse
+🍄 Pilz|pilz
+🥜 Erdnüsse|nüsse
+🍞 Brot|bäckerei
+🥐 Croissant|frühstück
+🥨 Brezel|bayern
+🧀 Käse|käse
+🥚 Ei|frühstück
+🍳 Spiegelei|frühstück braten
+🥞 Pfannkuchen|frühstück
+🥓 Speck|frühstück
+🍔 Hamburger|burger fastfood
+🍟 Pommes|fastfood
+🍕 Pizza|italienisch
+🌭 Hotdog|würstchen
+🥪 Sandwich|brot
+🌮 Taco|mexikanisch
+🥗 Salat|gesund
+🍝 Spaghetti|nudeln pasta
+🍜 Nudelsuppe|ramen
+🍣 Sushi|japanisch fisch
+🍦 Softeis|eis dessert
+🍩 Donut|süß
+🍪 Keks|süß
+🎂 Geburtstagskuchen|geburtstag torte feiern
+🍰 Kuchenstück|kuchen torte
+🧁 Cupcake|kuchen süß
+🍫 Schokolade|süß
+🍬 Bonbon|süß
+🍯 Honigtopf|honig
+☕ Heißgetränk|kaffee tee pause
+🍵 Tee|tee
+🥤 Becher mit Strohhalm|getränk
+🍺 Bier|getränk prost
+🍻 Anstoßende Biergläser|prost feiern
+🍷 Weinglas|wein getränk
+🥂 Sektgläser|anstoßen feiern prost
+🍾 Sektflasche|feiern
+🍽️ Teller mit Besteck|essen mahlzeit
+🚗 Auto|fahrzeug
+🚕 Taxi|fahrzeug
+🚌 Bus|fahrzeug verkehr
+🚑 Krankenwagen|notfall
+🚒 Feuerwehrauto|feuerwehr
+🚓 Polizeiauto|polizei
+🚚 Lieferwagen|lieferung lkw versand
+🚜 Traktor|landwirtschaft
+🚲 Fahrrad|rad
+🛴 Tretroller|roller
+🏍️ Motorrad|fahrzeug
+🚂 Lokomotive|zug bahn
+🚆 Zug|bahn reise
+🚇 U-Bahn|bahn
+✈️ Flugzeug|fliegen reise urlaub
+🛫 Abflug|fliegen reise
+🛬 Landung|fliegen ankunft
+🚀 Rakete|start launch schnell
+🛸 Ufo|außerirdisch
+🚁 Hubschrauber|fliegen
+⛵ Segelboot|boot segeln
+🚢 Schiff|boot reise
+⚓ Anker|schiff
+⛽ Zapfsäule|tanken benzin
+🚦 Ampel|verkehr
+🚧 Baustelle|arbeit sperrung
+🗺️ Weltkarte|karte reise
+🧭 Kompass|richtung navigation
+🏔️ Schneebedeckter Berg|berg
+🏖️ Strand mit Sonnenschirm|urlaub strand
+🏝️ Insel|urlaub
+🏠 Haus|zuhause heim
+🏡 Haus mit Garten|zuhause
+🏢 Bürogebäude|büro firma arbeit
+🏥 Krankenhaus|klinik
+🏦 Bank|geld
+🏨 Hotel|reise übernachtung
+🏫 Schule|bildung
+🏭 Fabrik|industrie
+🏰 Schloss|burg
+⛪ Kirche|religion
+🗼 Turm|tokio
+🗽 Freiheitsstatue|new york
+🎡 Riesenrad|kirmes
+🌃 Nacht mit Sternen|stadt nacht
+🌅 Sonnenaufgang|morgen
+⏰ Wecker|zeit wecker morgen
+⌛ Sanduhr|zeit warten
+⏱️ Stoppuhr|zeit messen
+🕐 Ein Uhr|uhr zeit
+⚽ Fußball|sport ball
+🏀 Basketball|sport ball
+🏈 Football|sport ball
+⚾ Baseball|sport ball
+🎾 Tennis|sport ball
+🏐 Volleyball|sport ball
+🏓 Tischtennis|sport
+🏸 Badminton|sport
+🥅 Tor|sport
+⛳ Golffahne|golf sport
+🎣 Angel|angeln fisch
+🎿 Ski|wintersport
+🏂 Snowboarder|wintersport
+🏊 Schwimmer|schwimmen sport
+🚴 Radfahrer|fahrrad sport
+🏆 Pokal|sieg gewinner preis
+🥇 Goldmedaille|erster sieg gewinner
+🥈 Silbermedaille|zweiter
+🥉 Bronzemedaille|dritter
+🏅 Sportmedaille|medaille preis
+🎖️ Orden|auszeichnung
+🎫 Eintrittskarte|ticket
+🎟️ Ticket|eintritt
+🎭 Theatermasken|theater kultur
+🎨 Farbpalette|kunst malen design
+🎬 Filmklappe|film kino
+🎤 Mikrofon|singen karaoke
+🎧 Kopfhörer|musik hören
+🎵 Musiknote|musik
+🎶 Musiknoten|musik
+🎹 Klaviatur|klavier musik
+🎸 Gitarre|musik
+🎮 Videospiel|spielen gaming controller
+🕹️ Joystick|spielen gaming
+🎲 Würfel|spiel glück
+🧩 Puzzleteil|puzzle lösung
+♟️ Schachfigur|schach strategie
+🎯 Volltreffer|ziel treffer
+🎳 Bowling|spiel
+🎉 Konfettibombe|feiern party glückwunsch
+🎊 Konfettiball|feiern party
+🎈 Luftballon|party geburtstag
+🎁 Geschenk|geburtstag überraschung
+🎄 Weihnachtsbaum|weihnachten
+🎃 Kürbislaterne|halloween
+✨ Funkeln|glanz neu magie
+⌚ Armbanduhr|uhr zeit
+📱 Mobiltelefon|handy smartphone
+💻 Laptop|computer rechner arbeit
+⌨️ Tastatur|computer tippen
+🖥️ Bildschirm|computer monitor
+🖨️ Drucker|drucken
+🖱️ Computermaus|maus klicken
+💾 Diskette|speichern
+💿 CD|daten musik
+📷 Kamera|foto
+📹 Videokamera|video
+📞 Telefonhörer|telefon anrufen
+☎️ Telefon|anrufen
+📺 Fernseher|tv
+📻 Radio|musik
+🔋 Batterie|akku energie
+🔌 Stecker|strom
+💡 Glühbirne|idee licht tipp
+🔦 Taschenlampe|licht
+🕯️ Kerze|licht
+🗑️ Papierkorb|löschen müll
+💰 Geldsack|geld reich
+💳 Kreditkarte|bezahlen geld
+💶 Euroschein|geld euro
+🧾 Beleg|quittung rechnung
+✉️ Briefumschlag|brief post mail
+📧 E-Mail|mail nachricht
+📨 Eingehender Umschlag|mail empfangen
+📦 Paket|versand lieferung
+📮 Briefkasten|post
+📝 Notiz|schreiben memo
+✏️ Bleistift|schreiben bearbeiten
+🖊️ Kugelschreiber|schreiben
+📁 Ordner|datei ablage
+📂 Geöffneter Ordner|datei
+📄 Dokument|datei seite
+📊 Balkendiagramm|statistik daten chart
+📈 Steigender Trend|wachstum statistik
+📉 Fallender Trend|rückgang statistik
+📅 Kalender|datum termin
+📆 Abreißkalender|datum termin
+📌 Pinnnadel|merken wichtig
+📍 Stecknadel|ort standort
+📎 Büroklammer|anhang
+📏 Lineal|messen
+✂️ Schere|schneiden
+🔒 Geschlossenes Schloss|sicher gesperrt privat
+🔓 Offenes Schloss|entsperrt
+🔑 Schlüssel|passwort zugang
+🔨 Hammer|werkzeug bauen
+🛠️ Werkzeuge|reparieren einstellungen
+⚙️ Zahnrad|einstellungen technik
+🧰 Werkzeugkasten|werkzeug
+🧲 Magnet|anziehen
+🧪 Reagenzglas|labor test chemie
+🔬 Mikroskop|wissenschaft forschung
+🔭 Teleskop|astronomie
+💊 Tablette|medizin
+🩺 Stethoskop|arzt medizin
+🛒 Einkaufswagen|einkaufen shop
+🎒 Rucksack|schule
+👓 Brille|lesen
+📚 Bücher|lesen lernen bibliothek
+📖 Offenes Buch|lesen
+🔍 Lupe|suchen suche
+🔔 Glocke|benachrichtigung
+📣 Megafon|ankündigung
+🏷️ Etikett|tag label preis
+🔗 Link|verknüpfung kette
+❤️ Rotes Herz|liebe herz
+🧡 Oranges Herz|herz
+💛 Gelbes Herz|herz
+💚 Grünes Herz|herz
+💙 Blaues Herz|herz
+💜 Lila Herz|herz
+🖤 Schwarzes Herz|herz
+🤍 Weißes Herz|herz
+💔 Gebrochenes Herz|trauer liebe
+💕 Zwei Herzen|liebe
+💯 Hundert Punkte|perfekt voll
+💢 Ärger-Symbol|wut
+💥 Zusammenstoß|knall boom
+💫 Schwindelig|sterne
+💬 Sprechblase|nachricht kommentar chat
+💭 Gedankenblase|denken
+💤 Schlafen|zzz müde
+✅ Weißes Häkchen auf Grün|erledigt ja fertig ok
+☑️ Abgehaktes Kästchen|erledigt
+✔️ Häkchen|ok richtig
+❌ Kreuz|nein falsch fehler löschen
+❎ Kreuz-Schaltfläche|nein
+➕ Plus|hinzufügen mehr
+➖ Minus|weniger entfernen
+❓ Fragezeichen|frage hilfe
+❗ Ausrufezeichen|wichtig achtung
+⚠️ Warnung|achtung vorsicht
+⛔ Zutritt verboten|stopp verbot
+🚫 Verboten|nein verbot
+♻️ Recycling|umwelt
+✳️ Achtzackiger Stern|stern
+🆕 Neu|neu
+🆗 OK|ok
+🆘 SOS|hilfe notfall
+🔴 Roter Kreis|rot punkt
+🟠 Oranger Kreis|orange punkt
+🟡 Gelber Kreis|gelb punkt
+🟢 Grüner Kreis|grün punkt
+🔵 Blauer Kreis|blau punkt
+🟣 Lila Kreis|lila punkt
+⚫ Schwarzer Kreis|schwarz punkt
+⚪ Weißer Kreis|weiß punkt
+🔺 Rotes Dreieck|nach oben
+🔻 Rotes Dreieck nach unten|nach unten
+⬆️ Pfeil nach oben|oben hoch
+⬇️ Pfeil nach unten|unten runter
+⬅️ Pfeil nach links|links zurück
+➡️ Pfeil nach rechts|rechts weiter
+🔄 Pfeile gegen den Uhrzeigersinn|aktualisieren neu laden
+🔁 Wiederholen|schleife
+▶️ Wiedergabe|abspielen play
+⏸️ Pause|pause
+⏹️ Stopp|stopp
+🔇 Stumm|ton aus
+🔊 Lautsprecher laut|ton an
+©️ Copyright|recht
+®️ Eingetragene Marke|marke
+™️ Markenzeichen|marke
+#️⃣ Raute|hashtag nummer
+🔢 Zahlen|nummer
+ℹ️ Information|info hinweis
+🏁 Zielflagge|ziel rennen fertig
+🚩 Dreiecksflagge|markierung warnung
+🏳️ Weiße Flagge|aufgeben
+🇩🇪 Flagge Deutschland|deutschland
+🇦🇹 Flagge Österreich|österreich
+🇨🇭 Flagge Schweiz|schweiz
+🇪🇺 Flagge Europäische Union|eu europa`,
+ label: 'Emojis',
+ search: 'Emoji suchen',
+ categoryNav: 'Kategorien',
+ categories: {
+ recent: 'Zuletzt verwendet',
+ smileys: 'Smileys & Gefühle',
+ people: 'Menschen & Gesten',
+ nature: 'Tiere & Natur',
+ food: 'Essen & Trinken',
+ travel: 'Reisen & Orte',
+ activities: 'Aktivitäten',
+ objects: 'Objekte',
+ symbols: 'Symbole',
+ } as Record,
+ results: 'Suchergebnisse',
+ empty: 'Keine Emojis gefunden',
+ count: (count: number) => (count === 0 ? 'Keine Emojis' : count === 1 ? '1 Emoji' : `${count} Emojis`),
+ },
+ ringProgress: { label: 'Fortschritt' },
+ sortable: {
+ instructions:
+ 'Leertaste oder Eingabetaste nimmt auf. Dann mit den Pfeiltasten verschieben, mit Leertaste ablegen oder mit Escape abbrechen. Mit der Maus: ziehen, oder klicken und an der Zielstelle erneut klicken.',
+ handle: (item: string) => `${item} verschieben`,
+ empty: 'Keine Einträge – hierher verschieben',
+ picked: (item: string, position: number, count: number) =>
+ `${item} aufgenommen, Position ${position} von ${count}.`,
+ moved: (item: string, position: number, count: number) => `${item}: Position ${position} von ${count}.`,
+ movedTo: (item: string, container: string, position: number, count: number) =>
+ `${item} in ${container}, Position ${position} von ${count}.`,
+ dropped: (item: string, position: number, count: number) =>
+ `${item} abgelegt, Position ${position} von ${count}.`,
+ cancelled: (item: string, position: number, count: number) =>
+ `Verschieben abgebrochen. ${item} wieder an Position ${position} von ${count}.`,
+ },
+ logViewer: {
+ label: 'Protokoll',
+ search: 'Protokoll durchsuchen',
+ levels: { debug: 'Debug', info: 'Info', warn: 'Warnung', error: 'Fehler' } as Record,
+ levelFilter: 'Ebenen',
+ autoScroll: 'Mitlaufen',
+ loadMore: 'Ältere Einträge laden',
+ jumpToEnd: 'Zum Ende',
+ lines: (shown: number, total: number) => (shown === total ? `${total} Zeilen` : `${shown} von ${total} Zeilen`),
+ empty: 'Keine Einträge',
+ paused: 'Mitlaufen angehalten',
+ },
+ kanban: {
+ /** React Native: action that moves a card to another column */
+ moveTo: (column: string) => `Nach „${column}“ verschieben`,
+ label: 'Board',
+ count: (count: number, limit?: number) => (limit ? `${count}/${limit}` : String(count)),
+ cards: (count: number, limit?: number) =>
+ `${count === 1 ? '1 Karte' : `${count} Karten`}${limit ? `, Limit ${limit}` : ''}`,
+ limitReached: 'WIP-Limit erreicht',
+ limitExceeded: (count: number, limit: number) => `WIP-Limit überschritten: ${count} von ${limit}`,
+ collapse: (column: string) => `${column} einklappen`,
+ expand: (column: string) => `${column} ausklappen`,
+ loadMore: 'Weitere laden',
+ empty: 'Keine Karten – hierher verschieben',
+ },
+ chatMessages: {
+ label: 'Nachrichten',
+ empty: 'Noch keine Nachrichten',
+ jump: 'Zu neuen Nachrichten',
+ newCount: (count: number) => (count === 1 ? '1 neue Nachricht' : `${count} neue Nachrichten`),
+ announce: (author: string, text: string) => `${author}: ${text}`,
+ today: 'Heute',
+ yesterday: 'Gestern',
+ loadMore: 'Ältere Nachrichten laden',
+ },
+ chatMessage: {
+ status: { sending: 'Wird gesendet', sent: 'Gesendet', read: 'Gelesen', error: 'Nicht gesendet' } as Record<
+ string,
+ string
+ >,
+ writing: 'Antwort wird erstellt',
+ attachments: 'Anhänge',
+ you: 'Sie',
+ assistant: 'Assistent',
+ },
+ chatPrompt: {
+ label: 'Nachricht',
+ placeholder: 'Nachricht schreiben …',
+ send: 'Senden',
+ stop: 'Antwort stoppen',
+ attach: 'Datei anhängen',
+ removeFile: (name: string) => `${name} entfernen`,
+ hint: 'Enter sendet, Umschalt+Enter für eine neue Zeile',
+ hintModEnter: 'Strg+Enter sendet',
+ },
+ chatSources: { title: (count: number) => (count === 1 ? '1 Quelle' : `${count} Quellen`) },
+ chatToolCall: {
+ title: (name: string) => `Werkzeug: ${name}`,
+ status: { running: 'Läuft …', done: 'Fertig', error: 'Fehler' } as Record,
+ input: 'Eingabe',
+ output: 'Ergebnis',
+ },
+ chatThinking: {
+ active: 'Denkt nach …',
+ done: 'Gedankengang',
+ duration: (seconds: number) => `${seconds} s nachgedacht`,
+ },
+ authForm: {
+ titles: {
+ signIn: 'Anmelden',
+ signUp: 'Konto erstellen',
+ reset: 'Passwort zurücksetzen',
+ code: 'Code eingeben',
+ } as Record,
+ descriptions: {
+ signIn: 'Mit E-Mail-Adresse und Passwort',
+ signUp: 'In wenigen Schritten zum eigenen Konto',
+ reset: 'Wir schicken Ihnen einen Link zum Zurücksetzen.',
+ code: 'Den Code aus der E-Mail oder der Authentifizierungs-App eingeben',
+ } as Record,
+ submit: { signIn: 'Anmelden', signUp: 'Registrieren', reset: 'Link senden', code: 'Bestätigen' } as Record<
+ string,
+ string
+ >,
+ name: 'Name',
+ email: 'E-Mail-Adresse',
+ password: 'Passwort',
+ passwordHint: (min: number) => `Mindestens ${min} Zeichen`,
+ code: 'Einmalcode',
+ remember: 'Angemeldet bleiben',
+ forgot: 'Passwort vergessen?',
+ or: 'oder',
+ providers: 'Anmelden mit',
+ noAccount: 'Noch kein Konto?',
+ signUpLink: 'Registrieren',
+ hasAccount: 'Schon ein Konto?',
+ signInLink: 'Anmelden',
+ back: 'Zurück zur Anmeldung',
+ resetSent: 'Falls ein Konto zu dieser Adresse existiert, ist ein Link unterwegs.',
+ sending: 'Wird gesendet …',
+ },
+ sessionTimeout: {
+ title: 'Sitzung läuft bald ab',
+ description: 'Aus Sicherheitsgründen werden Sie bei Inaktivität abgemeldet. Möchten Sie angemeldet bleiben?',
+ remaining: 'Verbleibende Zeit',
+ extend: 'Angemeldet bleiben',
+ signOut: 'Abmelden',
+ expiredTitle: 'Sitzung abgelaufen',
+ expired: 'Sie wurden aus Sicherheitsgründen abgemeldet.',
+ close: 'Schließen',
+ },
+ /** Messages of the validators (isRequired, isEmail …) */
+ validation: {
+ required: 'Pflichtfeld',
+ email: 'Bitte eine gültige E-Mail-Adresse eingeben',
+ url: 'Bitte eine gültige Adresse eingeben (https://…)',
+ minLength: (min: number) => `Mindestens ${min} Zeichen`,
+ maxLength: (max: number) => `Höchstens ${max} Zeichen`,
+ length: (min: number, max: number) => `${min} bis ${max} Zeichen`,
+ min: (min: string) => `Mindestens ${min}`,
+ max: (max: string) => `Höchstens ${max}`,
+ range: (min: string, max: string) => `Zwischen ${min} und ${max}`,
+ number: 'Bitte eine Zahl eingeben',
+ integer: 'Bitte eine ganze Zahl eingeben',
+ pattern: 'Das Format stimmt nicht',
+ matches: 'Die Eingaben stimmen nicht überein',
+ iban: 'Bitte eine gültige IBAN eingeben',
+ phone: 'Bitte eine gültige Telefonnummer eingeben',
+ },
+};
+
+export default de;
diff --git a/packages/recipes/src/native/index.ts b/packages/recipes/src/native/index.ts
index 4455484..51a2d97 100644
--- a/packages/recipes/src/native/index.ts
+++ b/packages/recipes/src/native/index.ts
@@ -35,6 +35,7 @@ export {
defaultLocale,
formatFileSize,
messages,
+ csvSeparator,
numberSeparators,
parseLocaleNumber,
pluralText,
diff --git a/packages/recipes/src/table-view.ts b/packages/recipes/src/table-view.ts
index 7a13951..a8820b2 100644
--- a/packages/recipes/src/table-view.ts
+++ b/packages/recipes/src/table-view.ts
@@ -226,7 +226,7 @@ export const exportColumns = (columns: readonly TableViewColumn[]) =>
/** Options of the CSV of a table */
export interface TableCsvOptions {
- /** Separator, default ";" (opens in a German Excel) */
+ /** Separator (default ";"; the DataTable passes its `csvSeparator`, by default from the locale) */
separator?: string;
/** Byte order mark at the start, so Excel reads UTF-8 – default true */
bom?: boolean;
diff --git a/packages/tokens/scripts/build-theme.ts b/packages/tokens/scripts/build-theme.ts
index eba4cde..686defa 100644
--- a/packages/tokens/scripts/build-theme.ts
+++ b/packages/tokens/scripts/build-theme.ts
@@ -20,8 +20,19 @@ const themeRules = themes
.map(themeToCss)
.join('\n\n');
+// Without data-mode on the page follows the system setting; data-mode="light"|"dark" forces a mode, also for a
+// part of the page (a forced light part stays light on a dark system)
const css = `/* Generated by @wirecore/wirelayer-tokens – do not edit */
-@custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *));
+@custom-variant dark {
+ &:where([data-mode=dark], [data-mode=dark] *) {
+ @slot;
+ }
+ @media (prefers-color-scheme: dark) {
+ &:where(:root:not([data-mode]), :root:not([data-mode]) *):not(:where([data-mode=light], [data-mode=light] *)) {
+ @slot;
+ }
+ }
+}
:root,
[data-mode=light] {
@@ -29,6 +40,14 @@ const css = `/* Generated by @wirecore/wirelayer-tokens – do not edit */
${colorDeclarations(colors.light)}
}
+@media (prefers-color-scheme: dark) {
+ /* Same specificity as [data-mode=dark], so the light overrides of a theme behave the same in both ways */
+ :root:where(:not([data-mode])) {
+ color-scheme: dark;
+${colorDeclarations(colors.dark).replace(/^/gm, ' ')}
+ }
+}
+
[data-mode=dark] {
color-scheme: dark;
${colorDeclarations(colors.dark)}
diff --git a/packages/tokens/src/css.ts b/packages/tokens/src/css.ts
index 8a1bc01..ed702c8 100644
--- a/packages/tokens/src/css.ts
+++ b/packages/tokens/src/css.ts
@@ -9,6 +9,7 @@ export const colorDeclarations = (set: Partial) =>
/**
* CSS for a theme: its overrides apply with `data-theme=""`, combined with `data-mode` on the same
* element or nested in either direction. Dark rules are more specific, so they win over the light ones.
+ * Without `data-mode` on the root the system setting decides, like for the base colours.
* Use it to generate CSS for a custom theme, e.g. in a build script.
*/
export function themeToCss(theme: Theme): string {
@@ -22,5 +23,12 @@ ${t}[data-mode=dark],
${t} [data-mode=dark],
[data-mode=dark] ${t} {
${colorDeclarations(theme.colors.dark)}
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-mode])${t},
+ :root:not([data-mode]) ${t}:not([data-mode], [data-mode=light] *) {
+${colorDeclarations(theme.colors.dark).replace(/^/gm, ' ')}
+ }
}`;
}
diff --git a/packages/tokens/src/tokens.ts b/packages/tokens/src/tokens.ts
index e42869f..cc5931d 100644
--- a/packages/tokens/src/tokens.ts
+++ b/packages/tokens/src/tokens.ts
@@ -1,4 +1,4 @@
-/** Colour schemes, switched with `data-mode` (`:root` = light). */
+/** Colour schemes, switched with `data-mode` – without it the system setting (`prefers-color-scheme`) decides. */
export const modes = ['light', 'dark'] as const;
export type Mode = (typeof modes)[number];
diff --git a/packages/tokens/test/theme.test.ts b/packages/tokens/test/theme.test.ts
index b556487..a2aea89 100644
--- a/packages/tokens/test/theme.test.ts
+++ b/packages/tokens/test/theme.test.ts
@@ -1,5 +1,6 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
+import { themes, themeToCss } from '../src/index.js';
const css = readFileSync(new URL('../dist/theme.css', import.meta.url), 'utf8');
@@ -30,3 +31,28 @@ describe('animation theme variables', () => {
for (const { inline, rule } of dynamic) expect(inline, rule).toBe(true);
});
});
+
+describe('light and dark', () => {
+ // Without data-mode the page follows the system; data-mode="light"|"dark" forces a mode (checked in the browser
+ // as well – this keeps the generated rules from getting lost)
+ it('uses the dark colours for a page without data-mode when the system is dark', () => {
+ expect(css).toMatch(
+ /@media \(prefers-color-scheme: dark\) \{\s*\/\*[^*]*\*\/\s*:root:where\(:not\(\[data-mode\]\)\) \{\s*color-scheme: dark;/
+ );
+ });
+
+ it('also applies the dark variant to a page without data-mode, except in a forced light part', () => {
+ expect(css).toContain('&:where([data-mode=dark], [data-mode=dark] *)');
+ expect(css).toContain(
+ '&:where(:root:not([data-mode]), :root:not([data-mode]) *):not(:where([data-mode=light], [data-mode=light] *))'
+ );
+ });
+
+ it('gives every theme with dark colours the system case, too', () => {
+ for (const theme of themes.filter((theme) => Object.keys(theme.colors.dark).length)) {
+ const rules = themeToCss(theme);
+ expect(rules, theme.name).toContain(`:root:not([data-mode])[data-theme=${theme.name}]`);
+ expect(css, theme.name).toContain(rules);
+ }
+ });
+});
diff --git a/packages/vue/.storybook/preview.ts b/packages/vue/.storybook/preview.ts
index 12d8e27..01810c6 100644
--- a/packages/vue/.storybook/preview.ts
+++ b/packages/vue/.storybook/preview.ts
@@ -10,6 +10,7 @@ import { h } from 'vue';
import { LocaleProvider, registerLocale } from '../src/index.js';
import { ar } from '../src/locales/ar.js';
import { cs } from '../src/locales/cs.js';
+import { de } from '../src/locales/de.js';
import { es } from '../src/locales/es.js';
import { fr } from '../src/locales/fr.js';
import { hu } from '../src/locales/hu.js';
@@ -23,7 +24,7 @@ import { tr } from '../src/locales/tr.js';
import './preview.css';
// The toolbar languages that are not built in – an app registers the ones it needs the same way
-const locales = { fr, es, it, pt, 'pt-BR': ptBR, nl, pl, cs, hu, lv, tr, ar };
+const locales = { de, fr, es, it, pt, 'pt-BR': ptBR, nl, pl, cs, hu, lv, tr, ar };
for (const [code, texts] of Object.entries(locales)) registerLocale(code, texts);
// The story inside LocaleProvider; lang and dir on like an app would set them
diff --git a/packages/vue/src/chart/Chart.vue b/packages/vue/src/chart/Chart.vue
index 3af42e4..c247000 100644
--- a/packages/vue/src/chart/Chart.vue
+++ b/packages/vue/src/chart/Chart.vue
@@ -60,6 +60,8 @@ const rows = computed(() => chartTable(props.data, format.value, translations.ch
let instance: Chart | undefined;
let observer: MutationObserver | undefined;
let motion: MediaQueryList | undefined;
+// Without data-mode the page follows the system – recolour when it switches
+let scheme: MediaQueryList | undefined;
function config() {
const theme = readChartTheme(canvas.value!);
@@ -99,10 +101,13 @@ onMounted(() => {
observer = new MutationObserver(update);
observer.observe(document.documentElement, { attributes: true });
motion.addEventListener('change', update);
+ scheme = matchMedia('(prefers-color-scheme: dark)');
+ scheme.addEventListener('change', update);
});
onBeforeUnmount(() => {
observer?.disconnect();
motion?.removeEventListener('change', update);
+ scheme?.removeEventListener('change', update);
instance?.destroy();
});
watch(() => props.type, create);
diff --git a/packages/vue/src/chart/ChartCanvas.vue b/packages/vue/src/chart/ChartCanvas.vue
index ab3f58a..b17bdc0 100644
--- a/packages/vue/src/chart/ChartCanvas.vue
+++ b/packages/vue/src/chart/ChartCanvas.vue
@@ -23,6 +23,8 @@ const canvas = useTemplateRef('canvas');
let instance: Chart | undefined;
let observer: MutationObserver | undefined;
let motion: MediaQueryList | undefined;
+// Without data-mode the page follows the system – recolour when it switches
+let scheme: MediaQueryList | undefined;
const config = () => props.build(readChartTheme(canvas.value!), !!motion?.matches) as unknown as ChartConfiguration;
function create() {
@@ -46,10 +48,13 @@ onMounted(() => {
observer = new MutationObserver(update);
observer.observe(document.documentElement, { attributes: true });
motion.addEventListener('change', update);
+ scheme = matchMedia('(prefers-color-scheme: dark)');
+ scheme.addEventListener('change', update);
});
onBeforeUnmount(() => {
observer?.disconnect();
motion?.removeEventListener('change', update);
+ scheme?.removeEventListener('change', update);
instance?.destroy();
});
watch(() => props.build, update);
diff --git a/packages/vue/src/chart/Sparkline.vue b/packages/vue/src/chart/Sparkline.vue
index abd30ed..5693657 100644
--- a/packages/vue/src/chart/Sparkline.vue
+++ b/packages/vue/src/chart/Sparkline.vue
@@ -42,6 +42,8 @@ const format = computed(
const summary = computed(() => sparklineSummary(props.label, props.data, format.value, translations.chart));
let instance: Chart<'line'> | undefined;
let observer: MutationObserver | undefined;
+// Without data-mode the page follows the system – recolour when it switches
+let scheme: MediaQueryList | undefined;
function config() {
const theme = readChartTheme(canvas.value!);
@@ -87,9 +89,12 @@ onMounted(() => {
if (context) instance = new Chart(context, { type: 'line', ...config() } as never);
observer = new MutationObserver(update);
observer.observe(document.documentElement, { attributes: true });
+ scheme = matchMedia('(prefers-color-scheme: dark)');
+ scheme.addEventListener('change', update);
});
onBeforeUnmount(() => {
observer?.disconnect();
+ scheme?.removeEventListener('change', update);
instance?.destroy();
});
watch(() => [...props.data], update);
diff --git a/packages/vue/src/components/EmojiPicker/EmojiPicker.stories.ts b/packages/vue/src/components/EmojiPicker/EmojiPicker.stories.ts
index 0165130..5bfc27b 100644
--- a/packages/vue/src/components/EmojiPicker/EmojiPicker.stories.ts
+++ b/packages/vue/src/components/EmojiPicker/EmojiPicker.stories.ts
@@ -20,7 +20,7 @@ const meta = {
export default meta;
type Story = StoryObj;
-/** Search (English names, German too in a German locale), categories, a grid with arrow keys; recent ones first */
+/** Search (English names, with a language module like German also its names), categories, a grid with arrow keys; recent ones first */
export const Playground: Story = {};
/** Own emojis (e.g. reactions) */
diff --git a/packages/vue/src/components/EmojiPicker/EmojiPicker.vue b/packages/vue/src/components/EmojiPicker/EmojiPicker.vue
index b2f06f9..dcc9524 100644
--- a/packages/vue/src/components/EmojiPicker/EmojiPicker.vue
+++ b/packages/vue/src/components/EmojiPicker/EmojiPicker.vue
@@ -67,9 +67,7 @@ const icons: Record = {
symbols: '❤️',
};
-const all = computed(
- () => props.emojis ?? localeEmojis(locale.value.locale, locale.value.translations.emojiPicker.emojis)
-);
+const all = computed(() => props.emojis ?? localeEmojis(t.value.emojis));
const stored = useLocalStorage(props.recentKey, []);
const recentEntries = computed(() =>
props.recent
@@ -135,8 +133,9 @@ function onSearchKeydown(event: KeyboardEvent) {
({
components: { Badge, DataTable: DataTable as ConcreteComponent, IconButton, Progress },
setup: () => ({
+ features,
args,
selection: ref
([]),
statusVariant,
@@ -96,6 +101,7 @@ const meta = {
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: fileRows,
columns: [
{ id: 'name', header: 'Name' },
@@ -188,6 +195,7 @@ export const Tree: Story = {
}),
template: `
({ ...host })));
const lastEdit = ref('–');
return {
+ features,
rows,
lastEdit,
columns: hostColumns,
@@ -257,6 +266,7 @@ export const Tools: Story = {
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: hosts,
columns: [
{ id: 'name', header: 'Name', sortable: true },
@@ -291,6 +302,7 @@ export const Grouped: Story = {
}),
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: machines(40),
columns: machineColumns,
card,
@@ -384,6 +397,7 @@ export const FilterMenu: Story = {
}),
template: `
{
const rows = ref(machines(5));
return {
+ features,
rows,
card,
getRowId: machineId,
@@ -506,6 +523,7 @@ export const RowEditing: Story = {
},
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: machines(12),
card,
getRowId: machineId,
@@ -548,6 +567,7 @@ export const Totals: Story = {
}),
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: widthRows,
card,
columns: machineColumns.slice(0, 3).map((column) => ({ ...column, filter: undefined, copyable: false })),
@@ -581,6 +602,7 @@ export const VirtualWidth: Story = {
}),
template: `
({
components: { Badge, Checkbox, DataTable: DataTable as ConcreteComponent },
- setup: () => ({ rows: services, columns: serviceColumns, card, getRowId: (row: ServiceRow) => row.id }),
+ setup: () => ({
+ features,
+ rows: services,
+ columns: serviceColumns,
+ card,
+ getRowId: (row: ServiceRow) => row.id,
+ }),
template: `
{
const rows = ref(machines(2000));
return {
+ features,
rows,
card,
columns: keyboardColumns,
@@ -791,6 +823,7 @@ export const Keyboard: Story = {
},
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: machines(12),
card,
columns: machineColumns.slice(0, 5).map((column) => ({ ...column, filter: undefined })),
@@ -823,6 +857,7 @@ export const KeyboardGrouped: Story = {
}),
template: `
string } | null>(null);
const csv = ref('');
return {
+ features,
table,
csv,
rows: machines(12),
@@ -874,6 +910,7 @@ export const StatusBar: Story = {
template: `
{
const last = ref('–');
return {
+ features,
last,
card,
rows: machines(6),
@@ -917,6 +955,7 @@ export const RowLinks: Story = {
template: `
({
components: { DataTable: DataTable as ConcreteComponent, Switch },
setup: () => ({
+ features,
loading: ref(true),
rows: machines(8),
card,
@@ -946,6 +986,7 @@ export const SkeletonLoading: Story = {
Loading
{
const rows = ref(plans.map((plan) => ({ ...plan })));
return {
+ features,
rows,
card,
columns: planColumns,
@@ -1042,6 +1084,7 @@ export const Editors: Story = {
},
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: machines(12),
columns: excelColumns,
card,
@@ -1099,6 +1143,7 @@ export const ExcelExport: Story = {
}),
template: `
([]);
return {
+ features,
rows,
marked,
problems,
@@ -1174,6 +1220,7 @@ export const Spreadsheet: Story = {
template: `
Promise } | null>(null);
const fail = ref(false);
return {
+ features,
table,
fail,
card,
@@ -1313,6 +1363,7 @@ export const ServerSource: Story = {
template: `
('sum');
return {
+ features,
rowsBy,
columnsBy,
aggregate,
@@ -1393,6 +1445,7 @@ export const Pivot: Story = {
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: mergedRows,
card,
getRowId: machineId,
@@ -1443,6 +1497,7 @@ export const MergedCells: Story = {
}),
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: machines(2000),
columns: keyboardColumns.map((column) => ({ ...column, editable: false })),
card,
@@ -1473,6 +1529,7 @@ export const Find: Story = {
}),
template: `
({
components: { Button, DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
table: ref<{ print: (options?: object) => void } | null>(null),
rows: machines(60),
columns: excelColumns,
@@ -1505,6 +1563,7 @@ export const Print: Story = {
}),
template: `
({
components: { DataTable: DataTable as ConcreteComponent },
setup: () => ({
+ features,
rows: machines(6),
card,
getRowId: machineId,
@@ -1631,6 +1694,7 @@ export const ColumnGroups: Story = {
}),
template: `
({
components: { DataTable: DataTable as ConcreteComponent, Field, NumberInput },
setup: () => ({
+ features,
rows: machines(40),
columns: machineColumns,
card,
@@ -1677,6 +1742,7 @@ export const AdvancedFilter: Story = {
}),
template: `
{
pinnedColumns?: { start: string[]; end: string[] };
/** Shows an export button; the CSV has the filtered and sorted rows of all pages (or the selection) */
exportFilename?: string;
+ /** Separator of the CSV export, also of the export button – default: from the locale (";" where numbers have a
+ * decimal comma, e.g. de-DE, as a spreadsheet there expects it; "," otherwise) */
+ csvSeparator?: string;
/** Formats of the export button: CSV (default), Excel (.xlsx) or both */
exportFormats?: ('csv' | 'xlsx')[];
/** Groups the rows by the values of these columns (expandable group rows) – v-model:group-by */
@@ -351,7 +356,13 @@ export interface DataTableProps {
fullscreenToggle?: boolean;
/** Fixed height (CSS length): the table scrolls inside, header and footer stay visible */
scrollHeight?: string;
- /** Only the visible rows are rendered (requires `scrollHeight`) */
+ /**
+ * Feature modules, imported on their own so apps only bundle what they use (fixed for the life of the table):
+ * `virtualRows` from `/virtual` for `virtual`, `dateInput` and `numberInput` for date and number filters and
+ * editors (without them the browser's inputs)
+ */
+ features?: DataTableFeature[];
+ /** Only the visible rows are rendered (requires `scrollHeight` and the feature `virtualRows`) */
virtual?: boolean;
/** Estimated row height in px for `virtual` */
rowHeight?: number;
@@ -390,6 +401,7 @@ const props = withDefaults(defineProps>(), {
columnOrder: undefined,
pinnedColumns: undefined,
exportFilename: undefined,
+ csvSeparator: undefined,
groupBy: undefined,
frozenRows: undefined,
rowActions: undefined,
@@ -481,8 +493,11 @@ const attrs = useAttrs();
const slots = useSlots();
const id = useId();
const t = translations.dataTable;
+// Read once: the virtualizer of `virtualRows` is a composable of this setup
+const feature: DataTableFeature = Object.assign({}, ...(props.features ?? []));
+provide(featureKey, feature);
-type Features = typeof features;
+type Features = typeof tableFeatures;
type TableRow = Row;
type TableColumn = Column;
type Density = 'sm' | 'md' | 'lg';
@@ -778,7 +793,7 @@ function toDef(column: DataTableColumn): ColumnDef {
const tableColumns = computed(() => columnDefs.value.map(toDef));
const table = useTable({
- features,
+ features: tableFeatures,
columns: tableColumns,
data: tableData,
getRowId: (row: TData, index: number) =>
@@ -2038,7 +2053,7 @@ function extendRangeByKey(event: KeyboardEvent) {
};
void nextTick(() => {
if (reveal() || !virtualOn.value) return;
- virtualizer.value.scrollToIndex(
+ virtualizer?.value.scrollToIndex(
items.value.findIndex((item) => item.kind === 'row' && item.row.id === next.focus.rowId)
);
requestAnimationFrame(() => requestAnimationFrame(() => void reveal()));
@@ -2231,7 +2246,7 @@ async function revealMatch() {
cellElement(match.rowId, match.columnId)?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
const index = items.value.findIndex((item) => item.kind === 'row' && item.row.id === match.rowId);
if (virtualOn.value && index >= 0) {
- virtualizer.value.scrollToIndex(index, { align: 'center' });
+ virtualizer?.value.scrollToIndex(index, { align: 'center' });
requestAnimationFrame(() => requestAnimationFrame(show));
} else show();
}
@@ -2371,7 +2386,8 @@ function generateCsv(options: DataTableCsvOptions = {}) {
: display(column, row.getValue(column.id), row.original)
)
);
- const csv = toCsv([visible.map((column) => column.exportHeader ?? column.header), ...records], options.separator);
+ const separator = options.separator ?? props.csvSeparator ?? localeCsvSeparator(localeContext.value.locale);
+ const csv = toCsv([visible.map((column) => column.exportHeader ?? column.header), ...records], separator);
return options.bom === false ? csv.slice(1) : csv;
}
/** Downloads the CSV – `true` (or `{ selectionOnly: true }`) only the selected rows */
@@ -2629,10 +2645,9 @@ onBeforeUnmount(() => restoreInert.splice(0).forEach((element) => (element.inert
// Scrolling: virtual rows and the end of the list
const scrollElement = useTemplateRef('scroll');
-const virtualOn = computed(() => !!props.virtual && !!props.scrollHeight);
-const virtualizer = useVirtualizer(
+const virtualizer = feature.useVirtualizer?.(
computed(() => ({
- count: virtualOn.value ? items.value.length : 0,
+ count: props.virtual && props.scrollHeight ? items.value.length : 0,
getScrollElement: () => scrollElement.value,
estimateSize: () => props.rowHeight,
overscan: 8,
@@ -2641,7 +2656,13 @@ const virtualizer = useVirtualizer(
initialRect: { width: 0, height: Number.parseInt(props.scrollHeight ?? '', 10) || 400 },
}))
);
-const virtualItems = computed(() => (virtualOn.value ? virtualizer.value.getVirtualItems() : []));
+if (props.virtual && !virtualizer) {
+ warnInDevelopment(
+ 'DataTable: `virtual` needs the feature virtualRows from @wirecore/wirelayer-vue/virtual (`:features="[virtualRows]"`) – all rows are rendered'
+ );
+}
+const virtualOn = computed(() => !!props.virtual && !!props.scrollHeight && !!virtualizer);
+const virtualItems = computed(() => (virtualOn.value ? (virtualizer?.value.getVirtualItems() ?? []) : []));
const shownItems = computed(() =>
virtualOn.value
? virtualItems.value.map((virtual) => ({ ...items.value[virtual.index]!, index: virtual.index }))
@@ -2649,10 +2670,10 @@ const shownItems = computed(() =>
);
const paddingTop = computed(() => (virtualOn.value ? (virtualItems.value[0]?.start ?? 0) : 0));
const paddingBottom = computed(() =>
- virtualOn.value ? virtualizer.value.getTotalSize() - (virtualItems.value.at(-1)?.end ?? 0) : 0
+ virtualOn.value ? (virtualizer?.value.getTotalSize() ?? 0) - (virtualItems.value.at(-1)?.end ?? 0) : 0
);
const measure = (element: unknown) => {
- if (virtualOn.value && element instanceof Element) virtualizer.value.measureElement(element);
+ if (virtualOn.value && element instanceof Element) virtualizer?.value.measureElement(element);
};
let endReported = false;
watch(
@@ -2711,7 +2732,7 @@ function onGridKey(event: KeyboardEvent) {
if (treeGrid.value && onTreeGridKey(event)) return;
gridKeydown(event, tableElement.value, {
itemCount: items.value.length,
- scrollToItem: virtualOn.value ? (index) => virtualizer.value.scrollToIndex(index) : undefined,
+ scrollToItem: virtualOn.value ? (index) => virtualizer?.value.scrollToIndex(index) : undefined,
pageRows:
scrollElement.value && props.scrollHeight
? Math.max(1, Math.floor(scrollElement.value.clientHeight / props.rowHeight) - 1)
diff --git a/packages/vue/src/data-table/ValueInput.vue b/packages/vue/src/data-table/ValueInput.vue
index d528987..de00ec3 100644
--- a/packages/vue/src/data-table/ValueInput.vue
+++ b/packages/vue/src/data-table/ValueInput.vue
@@ -1,13 +1,12 @@
@@ -62,22 +65,42 @@ const selected = computed(() => (byValue.value ? (props.modelValue ?? (props.any
:model-value="text"
@update:model-value="emit('update:modelValue', $event ?? '')"
/>
-
-
+
+
= Symbol('wirecore-data-table-features');
diff --git a/packages/vue/src/data-table/index.ts b/packages/vue/src/data-table/index.ts
index 10374b1..b034d84 100644
--- a/packages/vue/src/data-table/index.ts
+++ b/packages/vue/src/data-table/index.ts
@@ -1,5 +1,5 @@
-// DataTable on TanStack Table – requires @tanstack/vue-table (and @tanstack/vue-virtual for `virtual`) as optional
-// peer dependencies
+// DataTable on TanStack Table – requires @tanstack/vue-table as an optional peer dependency (virtual rows: the feature
+// virtualRows from /virtual with @tanstack/vue-virtual)
export { default as DataTable, type DataTableProps } from './DataTable.vue';
export type {
DataTableAdvancedFilterProps,
@@ -15,6 +15,7 @@ export type {
DataTableDataSource,
DataTableEditor,
DataTableEditorProps,
+ DataTableFeature,
DataTableFilterConstraint,
DataTableFilterInputProps,
DataTableFilterRule,
@@ -30,6 +31,8 @@ export type {
DataTableRowEdit,
DataTableRowReorder,
DataTableSort,
+ DataTableVirtualizer,
+ DataTableVirtualOptions,
DataTableXlsxOptions,
DataQuery,
DataRequest,
@@ -38,6 +41,8 @@ export type {
RowsRequest,
RowsResult,
} from './types.js';
+/** Feature modules for `features`: the DatePicker and NumberInput of date and number filters and editors */
+export { dateInput, numberInput } from './inputs.js';
/** Registers an own match mode for the filter menus of all tables (label, value input, test) */
export { defineMatchMode } from '@wirecore/wirelayer-recipes';
/** Moves a row in your data after a row-reorder event: `data = moveItem(data, from, to)` */
@@ -46,3 +51,6 @@ export { moveItem } from '@wirecore/wirelayer-recipes';
export { moveTreeRow } from '@wirecore/wirelayer-recipes';
/** A data source controller for own lists (abort, cache, debounced search) and the DataTable adapters */
export { createDataController, dataSourceToRows, rowsToDataSource } from '@wirecore/wirelayer-recipes';
+
+// Types of the recipes in the pivot props above
+export type { PivotValue } from '@wirecore/wirelayer-recipes';
diff --git a/packages/vue/src/data-table/inputs.ts b/packages/vue/src/data-table/inputs.ts
new file mode 100644
index 0000000..482f1cb
--- /dev/null
+++ b/packages/vue/src/data-table/inputs.ts
@@ -0,0 +1,8 @@
+import DatePicker from '../components/DatePicker/DatePicker.vue';
+import NumberInput from '../components/NumberInput/NumberInput.vue';
+import type { DataTableFeature } from './types.js';
+
+/** DataTable feature: the DatePicker for `date` and `datetime` filters and editors (without: the browser's date input) */
+export const dateInput: DataTableFeature = { dateInput: DatePicker };
+/** DataTable feature: the NumberInput for `number` filters and editors (without: the browser's number input) */
+export const numberInput: DataTableFeature = { numberInput: NumberInput };
diff --git a/packages/vue/src/data-table/types.ts b/packages/vue/src/data-table/types.ts
index d671c45..e555ab3 100644
--- a/packages/vue/src/data-table/types.ts
+++ b/packages/vue/src/data-table/types.ts
@@ -16,6 +16,7 @@ export type {
RowsRequest,
RowsResult,
} from '@wirecore/wirelayer-recipes';
+import type { Component, Ref } from 'vue';
import type {
ConditionField,
ConditionGroup,
@@ -159,7 +160,7 @@ export interface DataTableAllSelection {
export interface DataTableCsvOptions {
/** Only the selected rows (default: the filtered and sorted rows of all pages) */
selectionOnly?: boolean;
- /** Separator, default ";" (opens in a German Excel) */
+ /** Separator – default: `csvSeparator` of the table, else from the locale (";" with a decimal comma, "," otherwise) */
separator?: string;
/** Byte order mark at the start, so Excel reads UTF-8 – default true */
bom?: boolean;
@@ -280,3 +281,34 @@ export interface DataTableRowAction {
row: TData;
rowId: string;
}
+
+/** What the table gives the virtualizer of `virtualRows` */
+export interface DataTableVirtualOptions {
+ count: number;
+ getScrollElement: () => HTMLElement | null;
+ estimateSize: (index: number) => number;
+ overscan: number;
+ getItemKey: (index: number) => string | number;
+ initialRect: { width: number; height: number };
+}
+
+/** The virtualizer of `virtualRows`: which rows to render and where they are */
+export interface DataTableVirtualizer {
+ getVirtualItems(): { index: number; start: number; end: number }[];
+ getTotalSize(): number;
+ measureElement(element: Element): void;
+ scrollToIndex(index: number, options?: { align?: 'start' | 'center' | 'end' | 'auto' }): void;
+}
+
+/**
+ * A feature module for `features`, imported on its own so apps only bundle what they use: `virtualRows` (from
+ * `@wirecore/wirelayer-vue/data-table/virtual`, needs @tanstack/vue-virtual), `dateInput` and `numberInput`
+ */
+export interface DataTableFeature {
+ /** Renders only the visible rows with `virtual` */
+ useVirtualizer?: (options: Readonly[>) => Readonly][>;
+ /** Input of `date` and `datetime` filters and editors – without it the browser's date input */
+ dateInput?: Component;
+ /** Input of `number` filters and editors – without it the browser's number input */
+ numberInput?: Component;
+}
diff --git a/packages/vue/src/form/bind.ts b/packages/vue/src/form/bind.ts
index cf8a133..dc374fe 100644
--- a/packages/vue/src/form/bind.ts
+++ b/packages/vue/src/form/bind.ts
@@ -4,8 +4,9 @@ import type { CheckedState } from '../components/Checkbox/Checkbox.vue';
/** v-bind for Input and Textarea: `] ` */
export const bindInput = (field: AnyFieldApi) => ({
name: field.name,
- modelValue: field.state.value as string,
- 'onUpdate:modelValue': (value: string) => field.handleChange(value),
+ modelValue: (field.state.value as string | undefined) ?? '',
+ // Input and Textarea emit string | undefined (defineModel) – the field keeps a string, like in React
+ 'onUpdate:modelValue': (value: string | undefined) => field.handleChange(value ?? ''),
onBlur: field.handleBlur,
});
diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts
index f589922..b4a6804 100644
--- a/packages/vue/src/index.ts
+++ b/packages/vue/src/index.ts
@@ -445,3 +445,59 @@ export {
type UploadState,
} from '@wirecore/wirelayer-recipes';
export * from './composables/index.js';
+
+// Types of the recipes that appear in the props above – apps use them without depending on the recipes package
+export type {
+ AlertStatus,
+ AlertVariant,
+ AvatarColor,
+ AvatarShape,
+ AvatarSize,
+ AvatarVariant,
+ BadgeVariant,
+ BannerPosition,
+ BannerVariant,
+ ButtonColor,
+ ButtonShape,
+ ButtonSize,
+ ButtonVariant,
+ CarouselBreakpoint,
+ ChipSize,
+ DataQuery,
+ DataSource,
+ DateInput,
+ DateLike,
+ DialogPosition,
+ DialogStatus,
+ DividerOrientation,
+ DrawerPlacement,
+ DurationValueType,
+ HighlightColor,
+ HotkeyRegistry,
+ IconButtonVariant,
+ ItemValueOf,
+ LayoutGap,
+ MaskTokens,
+ MegaMenuCollapse,
+ MeterColor,
+ OverlaySize,
+ PasswordRequirement,
+ RadioGroupOrientation,
+ RecipeClasses,
+ RecipeConfig,
+ RecurrenceScope,
+ ResponsiveColumns,
+ ResultStatus,
+ SankeyLinkInput,
+ SankeyNodeInput,
+ SchedulerDropReason,
+ SegmentedControlSize,
+ Size,
+ SkeletonShape,
+ SpeedDialDirection,
+ SpeedDialLayout,
+ StatusDotStatus,
+ StepStatus,
+ TreeCheckedStrategy,
+ VisualTone,
+} from '@wirecore/wirelayer-recipes';
diff --git a/packages/vue/src/locale.ts b/packages/vue/src/locale.ts
index 37fba57..35a07ba 100644
--- a/packages/vue/src/locale.ts
+++ b/packages/vue/src/locale.ts
@@ -8,7 +8,7 @@ export function provideLocale(locale: ComputedRef) {
provide(key, locale);
}
-/** Locale, writing direction and texts of the nearest LocaleProvider – German without one */
+/** Locale, writing direction and texts of the nearest LocaleProvider – English (en-US) without one */
export function useLocale(): ComputedRef {
return inject(key, fallback);
}
diff --git a/packages/vue/src/locales/de.ts b/packages/vue/src/locales/de.ts
new file mode 100644
index 0000000..c127a38
--- /dev/null
+++ b/packages/vue/src/locales/de.ts
@@ -0,0 +1,2 @@
+// German texts: registerLocale('de', de) at app start, or pass them to LocaleProvider (translations)
+export { de, de as default } from '@wirecore/wirelayer-recipes/locales/de';
diff --git a/packages/vue/src/virtual/index.ts b/packages/vue/src/virtual/index.ts
index 6310325..d88a24d 100644
--- a/packages/vue/src/virtual/index.ts
+++ b/packages/vue/src/virtual/index.ts
@@ -2,3 +2,8 @@
export { default as VirtualSelect, type VirtualSelectProps } from './VirtualSelect.vue';
export { default as VirtualList, type VirtualListProps } from './VirtualList.vue';
export { default as VirtualTree, type VirtualTreeProps } from './VirtualTree.vue';
+/** DataTable feature: virtual rows for `virtual` */
+export { virtualRows } from './virtual-rows.js';
+
+// Type of the recipes in the props above (item accessors: a key or a function)
+export type { ItemAccessor } from '@wirecore/wirelayer-recipes';
diff --git a/packages/vue/src/virtual/virtual-rows.ts b/packages/vue/src/virtual/virtual-rows.ts
new file mode 100644
index 0000000..8adbf22
--- /dev/null
+++ b/packages/vue/src/virtual/virtual-rows.ts
@@ -0,0 +1,5 @@
+import { useVirtualizer } from '@tanstack/vue-virtual';
+import type { DataTableFeature } from '../data-table/types.js';
+
+/** DataTable feature: only the visible rows are rendered with `virtual` – `` */
+export const virtualRows: DataTableFeature = { useVirtualizer };
diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json
index 7bbf6cd..d1bd62a 100644
--- a/packages/vue/tsconfig.json
+++ b/packages/vue/tsconfig.json
@@ -5,5 +5,5 @@
"types": ["node"],
"noEmit": true
},
- "include": ["src", ".storybook", "vite.config.ts"]
+ "include": ["src", "typecheck", ".storybook", "vite.config.ts"]
}
diff --git a/packages/vue/typecheck/FormBindings.vue b/packages/vue/typecheck/FormBindings.vue
new file mode 100644
index 0000000..fe14adf
--- /dev/null
+++ b/packages/vue/typecheck/FormBindings.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+