feat(web): Uebersetzungsschluessel + Hilfsmodul calendar-month.ts fuer Kalender-Widget
- 16 neue Uebersetzungsschluessel unter widgets.calendar in de.json/en.json (Monatsnavigation, Naechste-Termine-Block, Einstellungsfelder) - Neues Hilfsmodul calendar-month.ts: resolveCalendarConfig, buildCalendarDays, groupEventsByDate, computeFetchWindow, selectUpcomingEvents, Formatierungsfunktionen (quick-260916-htc) - 8 Unit-Tests fuer das Hilfsmodul, Umlaut-Waechter weiterhin 3/3 gruen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { CalendarEvent } from '@/lib/calendar-api';
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
buildCalendarDays,
|
||||||
|
computeFetchWindow,
|
||||||
|
dateKey,
|
||||||
|
formatEventDate,
|
||||||
|
formatEventTime,
|
||||||
|
formatMonthLabel,
|
||||||
|
gridStartFor,
|
||||||
|
groupEventsByDate,
|
||||||
|
resolveCalendarConfig,
|
||||||
|
selectUpcomingEvents,
|
||||||
|
startOfLocalDay,
|
||||||
|
} from './calendar-month';
|
||||||
|
|
||||||
|
/** Kleine Fabrik fuer Testtermine (Muster aus dem Plan). */
|
||||||
|
function ev(id: string, start: Date, end: Date, extra?: Partial<CalendarEvent>): CalendarEvent {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
sourceId: 's1',
|
||||||
|
title: id,
|
||||||
|
start: start.toISOString(),
|
||||||
|
end: end.toISOString(),
|
||||||
|
allDay: false,
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('calendar-month', () => {
|
||||||
|
it('Test 1: buildCalendarDays liefert 42 Zellen ab Montag mit korrektem Monatsflag und heutigem Tag', () => {
|
||||||
|
const days = buildCalendarDays(new Date(2026, 6, 1), new Map(), new Date(2026, 6, 15));
|
||||||
|
|
||||||
|
expect(days).toHaveLength(42);
|
||||||
|
expect(days[0].key).toBe('2026-06-29');
|
||||||
|
expect(days[0].inCurrentMonth).toBe(false);
|
||||||
|
expect(days[2].key).toBe('2026-07-01');
|
||||||
|
expect(days[2].inCurrentMonth).toBe(true);
|
||||||
|
expect(days[41].key).toBe('2026-08-09');
|
||||||
|
|
||||||
|
const todayCells = days.filter((d) => d.isToday);
|
||||||
|
expect(todayCells).toHaveLength(1);
|
||||||
|
expect(todayCells[0].key).toBe('2026-07-15');
|
||||||
|
|
||||||
|
const outside = days.filter((d) => !d.inCurrentMonth);
|
||||||
|
expect(outside).toHaveLength(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 2: groupEventsByDate gruppiert nach lokalem Startdatum, buildCalendarDays uebernimmt die Zaehlung', () => {
|
||||||
|
const events = [
|
||||||
|
ev('e1', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||||
|
ev('e2', new Date(2026, 6, 20, 14, 0), new Date(2026, 6, 20, 15, 0)),
|
||||||
|
ev('e3', new Date(2026, 6, 21, 10, 0), new Date(2026, 6, 21, 11, 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
const grouped = groupEventsByDate(events);
|
||||||
|
expect(grouped.size).toBe(2);
|
||||||
|
expect(grouped.get('2026-07-20')).toHaveLength(2);
|
||||||
|
|
||||||
|
const days = buildCalendarDays(new Date(2026, 6, 1), grouped);
|
||||||
|
const day20 = days.find((d) => d.key === '2026-07-20');
|
||||||
|
expect(day20?.events.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 3: resolveCalendarConfig klemmt ungueltige Werte auf die Vorgaben/Grenzen zurueck', () => {
|
||||||
|
expect(resolveCalendarConfig({})).toEqual({ showMonth: true, maxEvents: 3, lookaheadDays: 30 });
|
||||||
|
expect(resolveCalendarConfig({ showMonth: false }).showMonth).toBe(false);
|
||||||
|
expect(resolveCalendarConfig({ showMonth: 'nein' }).showMonth).toBe(true);
|
||||||
|
expect(resolveCalendarConfig({ maxEvents: 99 }).maxEvents).toBe(10);
|
||||||
|
expect(resolveCalendarConfig({ maxEvents: -1 }).maxEvents).toBe(0);
|
||||||
|
expect(resolveCalendarConfig({ maxEvents: 4.7 }).maxEvents).toBe(4);
|
||||||
|
expect(resolveCalendarConfig({ maxEvents: '5' }).maxEvents).toBe(3);
|
||||||
|
expect(resolveCalendarConfig({ lookaheadDays: 45 }).lookaheadDays).toBe(30);
|
||||||
|
expect(resolveCalendarConfig({ lookaheadDays: 90 }).lookaheadDays).toBe(90);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 4: computeFetchWindow deckt Raster und Vorschau-Zeitraum als Tagesgrenzen ab', () => {
|
||||||
|
const w1 = computeFetchWindow(new Date(2026, 6, 1), 30, new Date(2026, 6, 15, 10, 30));
|
||||||
|
expect(w1.from.getTime()).toBe(new Date(2026, 5, 29).getTime());
|
||||||
|
expect(w1.to.getTime()).toBe(new Date(2026, 7, 14).getTime());
|
||||||
|
|
||||||
|
const w2 = computeFetchWindow(new Date(2026, 6, 1), 30, new Date(2026, 4, 1, 8, 0));
|
||||||
|
expect(w2.from.getTime()).toBe(new Date(2026, 4, 1).getTime());
|
||||||
|
expect(w2.to.getTime()).toBe(new Date(2026, 7, 10).getTime());
|
||||||
|
|
||||||
|
const w3 = computeFetchWindow(new Date(2026, 6, 1), 90, new Date(2026, 6, 15, 10, 30));
|
||||||
|
expect(w3.to.getTime()).toBe(new Date(2026, 9, 13).getTime());
|
||||||
|
|
||||||
|
for (const w of [w1, w2, w3]) {
|
||||||
|
for (const d of [w.from, w.to]) {
|
||||||
|
expect(d.getHours()).toBe(0);
|
||||||
|
expect(d.getMinutes()).toBe(0);
|
||||||
|
expect(d.getSeconds()).toBe(0);
|
||||||
|
expect(d.getMilliseconds()).toBe(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 5: selectUpcomingEvents filtert Vergangenheit/Horizont, sortiert nach Start und kuerzt auf maxEvents', () => {
|
||||||
|
const now = new Date(2026, 6, 15, 10, 0);
|
||||||
|
const events = [
|
||||||
|
ev('gestern', new Date(2026, 6, 14, 11, 0), new Date(2026, 6, 14, 12, 0)),
|
||||||
|
ev('läuft', new Date(2026, 6, 15, 9, 0), new Date(2026, 6, 15, 11, 0)),
|
||||||
|
ev('heute 15', new Date(2026, 6, 15, 15, 0), new Date(2026, 6, 15, 16, 0)),
|
||||||
|
ev('in 5 Tagen', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||||
|
ev('in 10 Tagen', new Date(2026, 6, 25, 9, 0), new Date(2026, 6, 25, 10, 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = selectUpcomingEvents(events, 7, 2, now);
|
||||||
|
expect(result.map((e) => e.title)).toEqual(['läuft', 'heute 15']);
|
||||||
|
|
||||||
|
expect(selectUpcomingEvents(events, 7, 0, now)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 6: formatEventDate/formatEventTime/formatMonthLabel formatieren de-DE', () => {
|
||||||
|
const timed = ev('t1', new Date(2026, 6, 20, 9, 5), new Date(2026, 6, 20, 10, 0));
|
||||||
|
const timedText = formatEventDate(timed);
|
||||||
|
expect(timedText).toMatch(/20\.07\./);
|
||||||
|
expect(timedText).toMatch(/09:05/);
|
||||||
|
|
||||||
|
const allDay = ev('t2', new Date(2026, 6, 20, 0, 0), new Date(2026, 6, 20, 23, 59), { allDay: true });
|
||||||
|
const allDayText = formatEventDate(allDay);
|
||||||
|
expect(allDayText).toMatch(/20\.07\./);
|
||||||
|
expect(allDayText).not.toMatch(/\d{2}:\d{2}/);
|
||||||
|
|
||||||
|
expect(formatMonthLabel(new Date(2026, 6, 1))).toBe('Juli 2026');
|
||||||
|
expect(formatEventTime(new Date(2026, 6, 20, 9, 5).toISOString())).toBe('09:05');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 7: gridStartFor findet den Montag am oder vor dem 1. des Monats', () => {
|
||||||
|
expect(gridStartFor(new Date(2026, 7, 1)).getTime()).toBe(new Date(2026, 6, 27).getTime());
|
||||||
|
expect(gridStartFor(new Date(2026, 5, 1)).getTime()).toBe(new Date(2026, 5, 1).getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Zusatz: dateKey/startOfLocalDay/addDays arbeiten lokal ohne UTC-Versatz', () => {
|
||||||
|
expect(dateKey(new Date(2026, 6, 5))).toBe('2026-07-05');
|
||||||
|
const start = startOfLocalDay(new Date(2026, 6, 5, 14, 30, 15));
|
||||||
|
expect(start.getHours()).toBe(0);
|
||||||
|
expect(addDays(new Date(2026, 6, 30), 2).getDate()).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* calendar-month — reine Hilfsfunktionen fuer das Kalender-Widget
|
||||||
|
* (quick-260916-htc).
|
||||||
|
*
|
||||||
|
* Kein React, keine DOM-Zugriffe: dieses Modul wird sowohl vom Widget
|
||||||
|
* (calendar-widget.tsx) als auch vom Einstellungsfeld
|
||||||
|
* (widget-settings-panel.tsx, `CalendarConfig`) importiert, damit beide
|
||||||
|
* Stellen dieselben Vorgaben/Grenzen anwenden (Muster clock-font-size.ts,
|
||||||
|
* T-BWO-01 uebertragen auf T-HTC-01).
|
||||||
|
*
|
||||||
|
* Vorlage fuer Monatsraster/Terminliste: das alte persoenliche Dashboard
|
||||||
|
* des Anwenders (user-files/personal-dashboard/src/app/page.tsx).
|
||||||
|
*
|
||||||
|
* Starttag-Regel: mehrtaegige und ganztaegige Termine werden im Monatsraster
|
||||||
|
* bewusst NUR am Starttag gezaehlt/angezeigt (`groupEventsByDate` gruppiert
|
||||||
|
* ausschliesslich nach `event.start`) — eine Terminleiste ueber mehrere Tage
|
||||||
|
* ist nicht Teil dieses Auftrags.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CalendarEvent } from '@/lib/calendar-api';
|
||||||
|
|
||||||
|
export const CALENDAR_LOOKAHEAD_OPTIONS: readonly number[] = [7, 14, 30, 60, 90];
|
||||||
|
export const CALENDAR_MAX_EVENTS_LIMIT = 10;
|
||||||
|
export const CALENDAR_DEFAULTS = { showMonth: true, maxEvents: 3, lookaheadDays: 30 } as const;
|
||||||
|
export const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] as const;
|
||||||
|
|
||||||
|
export interface CalendarWidgetConfig {
|
||||||
|
showMonth: boolean;
|
||||||
|
maxEvents: number;
|
||||||
|
lookaheadDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loest die Kalender-Widget-Konfiguration aus dem rohen `config`-JSON auf und
|
||||||
|
* klemmt ungueltige Werte auf die Vorgaben zurueck (T-HTC-01). API prueft
|
||||||
|
* Config-Felder nicht (`@IsObject()`), deshalb entscheidet ALLEIN diese
|
||||||
|
* Funktion, was ein gueltiger Wert ist.
|
||||||
|
*/
|
||||||
|
export function resolveCalendarConfig(config: Record<string, unknown>): CalendarWidgetConfig {
|
||||||
|
const showMonth = config.showMonth !== false;
|
||||||
|
|
||||||
|
let maxEvents: number = CALENDAR_DEFAULTS.maxEvents;
|
||||||
|
if (typeof config.maxEvents === 'number' && Number.isFinite(config.maxEvents)) {
|
||||||
|
maxEvents = Math.min(CALENDAR_MAX_EVENTS_LIMIT, Math.max(0, Math.trunc(config.maxEvents)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let lookaheadDays: number = CALENDAR_DEFAULTS.lookaheadDays;
|
||||||
|
if (
|
||||||
|
typeof config.lookaheadDays === 'number' &&
|
||||||
|
CALENDAR_LOOKAHEAD_OPTIONS.includes(config.lookaheadDays)
|
||||||
|
) {
|
||||||
|
lookaheadDays = config.lookaheadDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { showMonth, maxEvents, lookaheadDays };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lokaler Tagesschluessel `YYYY-MM-DD` (kein UTC-Versatz). */
|
||||||
|
export function dateKey(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kopie von `date` auf die lokale Tagesgrenze 00:00:00.000 gesetzt. */
|
||||||
|
export function startOfLocalDay(date: Date): Date {
|
||||||
|
const copy = new Date(date);
|
||||||
|
copy.setHours(0, 0, 0, 0);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kopie von `date`, um `days` Tage verschoben. Rechnet ueber `setDate`, NICHT
|
||||||
|
* ueber Millisekunden — das haelt die lokale Wanduhrzeit ueber
|
||||||
|
* Sommerzeitwechsel hinweg stabil.
|
||||||
|
*/
|
||||||
|
export function addDays(date: Date, days: number): Date {
|
||||||
|
const copy = new Date(date);
|
||||||
|
copy.setDate(copy.getDate() + days);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Montag am oder vor dem 1. des Monats von `monthDate`, 00:00 lokal. */
|
||||||
|
export function gridStartFor(monthDate: Date): Date {
|
||||||
|
const firstDayOfMonth = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
|
||||||
|
const mondayBasedStartOffset = (firstDayOfMonth.getDay() + 6) % 7;
|
||||||
|
const gridStart = new Date(firstDayOfMonth);
|
||||||
|
gridStart.setDate(firstDayOfMonth.getDate() - mondayBasedStartOffset);
|
||||||
|
gridStart.setHours(0, 0, 0, 0);
|
||||||
|
return gridStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendarDay {
|
||||||
|
key: string;
|
||||||
|
date: Date;
|
||||||
|
inCurrentMonth: boolean;
|
||||||
|
isToday: boolean;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gruppiert Termine nach lokalem Startdatum. Mehrtaegige/ganztaegige Termine
|
||||||
|
* zaehlen bewusst nur am Starttag (siehe Kopfkommentar).
|
||||||
|
*/
|
||||||
|
export function groupEventsByDate(events: CalendarEvent[]): Map<string, CalendarEvent[]> {
|
||||||
|
const groupedEvents = new Map<string, CalendarEvent[]>();
|
||||||
|
for (const event of events) {
|
||||||
|
const key = dateKey(new Date(event.start));
|
||||||
|
const existingEvents = groupedEvents.get(key) ?? [];
|
||||||
|
groupedEvents.set(key, [...existingEvents, event]);
|
||||||
|
}
|
||||||
|
return groupedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 42 Rasterzellen ab `gridStartFor(monthDate)`, Montag-basiert. */
|
||||||
|
export function buildCalendarDays(
|
||||||
|
monthDate: Date,
|
||||||
|
eventsByDate: Map<string, CalendarEvent[]>,
|
||||||
|
today: Date = new Date(),
|
||||||
|
): CalendarDay[] {
|
||||||
|
const month = monthDate.getMonth();
|
||||||
|
const gridStart = gridStartFor(monthDate);
|
||||||
|
const todayKey = dateKey(today);
|
||||||
|
|
||||||
|
const days: CalendarDay[] = [];
|
||||||
|
for (let index = 0; index < 42; index += 1) {
|
||||||
|
const date = addDays(gridStart, index);
|
||||||
|
const key = dateKey(date);
|
||||||
|
days.push({
|
||||||
|
key,
|
||||||
|
date,
|
||||||
|
inCurrentMonth: date.getMonth() === month,
|
||||||
|
isToday: key === todayKey,
|
||||||
|
events: eventsByDate.get(key) ?? [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ladefenster fuer `fetchEvents(from, to)`: deckt sowohl das 42-Tage-Raster
|
||||||
|
* als auch den Vorschau-Zeitraum (lookaheadDays ab heute) ab. Alle vier
|
||||||
|
* Zwischenwerte sind Tagesgrenzen 00:00 lokal, deshalb ist `toISOString()`
|
||||||
|
* innerhalb eines Tages konstant — das haelt den Backend-Cache-Schluessel
|
||||||
|
* (`${userId}:${from.toISOString()}:${to.toISOString()}`) ueber den
|
||||||
|
* 5-Minuten-Refresh hinweg stabil.
|
||||||
|
*/
|
||||||
|
export function computeFetchWindow(
|
||||||
|
monthDate: Date,
|
||||||
|
lookaheadDays: number,
|
||||||
|
now: Date = new Date(),
|
||||||
|
): { from: Date; to: Date } {
|
||||||
|
const gridStart = gridStartFor(monthDate);
|
||||||
|
const gridEnd = addDays(gridStart, 42);
|
||||||
|
const todayStart = startOfLocalDay(now);
|
||||||
|
const lookEnd = addDays(todayStart, lookaheadDays);
|
||||||
|
|
||||||
|
const from = gridStart.getTime() <= todayStart.getTime() ? gridStart : todayStart;
|
||||||
|
const to = gridEnd.getTime() >= lookEnd.getTime() ? gridEnd : lookEnd;
|
||||||
|
|
||||||
|
return { from, to };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waehlt die naechsten anstehenden Termine (laufend oder in der Zukunft,
|
||||||
|
* innerhalb `lookaheadDays`), sortiert nach Start aufsteigend, gekuerzt auf
|
||||||
|
* `maxEvents`.
|
||||||
|
*/
|
||||||
|
export function selectUpcomingEvents(
|
||||||
|
events: CalendarEvent[],
|
||||||
|
lookaheadDays: number,
|
||||||
|
maxEvents: number,
|
||||||
|
now: Date = new Date(),
|
||||||
|
): CalendarEvent[] {
|
||||||
|
const horizon = addDays(startOfLocalDay(now), lookaheadDays).getTime();
|
||||||
|
const nowTime = now.getTime();
|
||||||
|
|
||||||
|
return events
|
||||||
|
.filter((event) => {
|
||||||
|
const endTime = new Date(event.end).getTime();
|
||||||
|
const startTime = new Date(event.start).getTime();
|
||||||
|
return endTime >= nowTime && startTime < horizon;
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
|
||||||
|
.slice(0, maxEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Datum+Uhrzeit (bzw. nur Datum bei ganztaegig) im Format „Mi., 20.07., 09:05“. */
|
||||||
|
export function formatEventDate(event: CalendarEvent): string {
|
||||||
|
const start = new Date(event.start);
|
||||||
|
if (event.allDay) {
|
||||||
|
return new Intl.DateTimeFormat('de-DE', {
|
||||||
|
weekday: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
}).format(start);
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('de-DE', {
|
||||||
|
weekday: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nur die Uhrzeit eines ISO-Zeitstempels, „09:05“. */
|
||||||
|
export function formatEventTime(iso: string): string {
|
||||||
|
return new Intl.DateTimeFormat('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(new Date(iso));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Monat+Jahr, „Juli 2026“. */
|
||||||
|
export function formatMonthLabel(date: Date): string {
|
||||||
|
return new Intl.DateTimeFormat('de-DE', {
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
@@ -208,6 +208,22 @@
|
|||||||
"loading": "Laden...",
|
"loading": "Laden...",
|
||||||
"emptyNoSources": "Keine Kalenderquellen konfiguriert",
|
"emptyNoSources": "Keine Kalenderquellen konfiguriert",
|
||||||
"emptyNoEvents": "Keine anstehenden Termine",
|
"emptyNoEvents": "Keine anstehenden Termine",
|
||||||
|
"nothingSelected": "Nichts zum Anzeigen ausgewählt",
|
||||||
|
"monthPrev": "Zurück",
|
||||||
|
"monthNext": "Weiter",
|
||||||
|
"monthToday": "Zurück zum heutigen Monat",
|
||||||
|
"upcomingTitle": "Nächste Termine",
|
||||||
|
"tooltipMore": "Weitere Termine vorhanden",
|
||||||
|
"allDay": "ganztägig",
|
||||||
|
"configShowMonth": "Monatsansicht anzeigen",
|
||||||
|
"configMaxEvents": "Anzahl Termine",
|
||||||
|
"configMaxEventsNone": "Ausblenden",
|
||||||
|
"configMaxEventsOne": "1 Termin",
|
||||||
|
"configMaxEventsMany": "{count} Termine",
|
||||||
|
"configLookahead": "Zeitraum",
|
||||||
|
"configLookaheadOption": "Nächste {days} Tage",
|
||||||
|
"configSourcesHint": "Kalenderquellen verwalten Sie unter",
|
||||||
|
"configSourcesLink": "Einstellungen → Dashboard → Kalender",
|
||||||
"connectionSuccess": "Verbindung erfolgreich",
|
"connectionSuccess": "Verbindung erfolgreich",
|
||||||
"connectionError": "Verbindung fehlgeschlagen. Bitte überprüfen Sie die URL und Zugangsdaten.",
|
"connectionError": "Verbindung fehlgeschlagen. Bitte überprüfen Sie die URL und Zugangsdaten.",
|
||||||
"addSource": "Quelle hinzufügen",
|
"addSource": "Quelle hinzufügen",
|
||||||
|
|||||||
@@ -208,6 +208,22 @@
|
|||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"emptyNoSources": "No calendar sources configured",
|
"emptyNoSources": "No calendar sources configured",
|
||||||
"emptyNoEvents": "No upcoming events",
|
"emptyNoEvents": "No upcoming events",
|
||||||
|
"nothingSelected": "Nothing selected to display",
|
||||||
|
"monthPrev": "Back",
|
||||||
|
"monthNext": "Next",
|
||||||
|
"monthToday": "Back to the current month",
|
||||||
|
"upcomingTitle": "Upcoming events",
|
||||||
|
"tooltipMore": "More events available",
|
||||||
|
"allDay": "all day",
|
||||||
|
"configShowMonth": "Show month view",
|
||||||
|
"configMaxEvents": "Number of events",
|
||||||
|
"configMaxEventsNone": "Hide",
|
||||||
|
"configMaxEventsOne": "1 event",
|
||||||
|
"configMaxEventsMany": "{count} events",
|
||||||
|
"configLookahead": "Time range",
|
||||||
|
"configLookaheadOption": "Next {days} days",
|
||||||
|
"configSourcesHint": "Manage calendar sources under",
|
||||||
|
"configSourcesLink": "Settings → Dashboard → Calendar",
|
||||||
"connectionSuccess": "Connection successful",
|
"connectionSuccess": "Connection successful",
|
||||||
"connectionError": "Connection failed. Please check the URL and credentials.",
|
"connectionError": "Connection failed. Please check the URL and credentials.",
|
||||||
"addSource": "Add source",
|
"addSource": "Add source",
|
||||||
|
|||||||
Reference in New Issue
Block a user