diff --git a/TESTING.md b/TESTING.md
index 9550750..71220ff 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -1562,6 +1562,40 @@ bleibt `node` (schnell); wer ein DOM braucht, schreibt
---
+## Wochentage und Monate aus `Intl` (20.09.2026)
+
+Sechs handgeschriebene Arrays in drei Dateien, und `Intl` kam im ganzen Projekt
+nicht vor – obwohl jeder Browser es seit Jahren mitbringt.
+
+Die Doppelpflege war nicht nur lästig, sie war eine Falle: `DayDialog` begann
+seine Woche am **Sonntag**, die anderen am **Montag**, jeweils passend zu ihrer
+Indizierung. Beide waren für sich richtig. Wer aber eines der Arrays in eine
+neue Ansicht kopiert hätte, hätte sich einen Off-by-one gebaut, der nur an
+sieben Tagen im Jahr auffällt.
+
+Jetzt eine Ordnung (Montag zuerst, wie `weekdayIndex`) plus eine ausdrücklich
+benannte zweite Liste `weekdayLongNamesSun` für alles, was direkt mit
+`getDay()` indiziert.
+
+**Verhaltensgleich belegt:** Die erzeugten Namen stimmen zeichengenau mit den
+gelöschten Arrays überein – vor dem Austausch verglichen, nicht danach gehofft.
+
+**Die Sprache ist fest auf `en` gesetzt, nicht die des Browsers.**
+`Intl.DateTimeFormat(undefined, …)` nähme die Einstellung des Rechners und
+setzte auf einem deutschen System „Montag" neben ein englisches „Repeat" –
+genau der Mischmasch, der am 18.09. ausgeräumt wurde. Kommt eine Sprachwahl,
+ist `uiLocale` in `dates.ts` die eine Stelle, an der sie andockt.
+
+- [x] Fünf Tests in `dates.test.ts`, unter Mutation nachgewiesen: Nimmt man
+ dem Sonntag seinen Platz vorn, wird der Test rot.
+- [ ] Monatsansicht: Kopfzeile Mon–Sun, Termine am richtigen Tag 🔎
+- [ ] Wochenansicht: Datumszeile stimmt mit dem Wochentag überein
+- [ ] Tagesdialog: Überschrift nennt den richtigen Wochentag (der Fall mit
+ der Sonntags-Liste) 🔎
+- [ ] Serientermin „An diesen Tagen": Beschriftung passt zum gesetzten Tag –
+ anlegen, in DAVx5 prüfen, ob derselbe Wochentag ankommt
+- [ ] Listenansicht: Datum als „September 18, 2026"
+
## Platzhalter und Leerzustände (18.09.2026)
Beim Laden stand ein Kreisel in der Mitte, danach klappte das Layout um. Jetzt
diff --git a/frontend/src/components/calendar/DayDialog.tsx b/frontend/src/components/calendar/DayDialog.tsx
index 356e303..4e3e135 100644
--- a/frontend/src/components/calendar/DayDialog.tsx
+++ b/frontend/src/components/calendar/DayDialog.tsx
@@ -1,12 +1,8 @@
import { useEffect } from 'react'
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss'
import type { CalendarEvent } from '../../api/client'
+import { weekdayLongNamesSun, monthLongNames } from './dates'
-const weekdayLong = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
-const monthLabels = [
- 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December',
-]
export interface DayEvent extends CalendarEvent {
calendarId: string
@@ -60,9 +56,9 @@ export default function DayDialog({ day, events, onPick, onCreate, onClose }: {
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}>
diff --git a/frontend/src/components/calendar/EventDialog.tsx b/frontend/src/components/calendar/EventDialog.tsx
index 7c734e4..47941a6 100644
--- a/frontend/src/components/calendar/EventDialog.tsx
+++ b/frontend/src/components/calendar/EventDialog.tsx
@@ -4,6 +4,7 @@ import type { CalendarEvent, CalendarItem, EventInput, Recurrence, WeekdayCode }
import {
addDays, allDayISO, dateFromInput, eventDay, nthOfMonth, timedISO,
toDateInput, toTimeInput, weekdayCodeOf, weekdayCodes,
+ weekdayShortNames, weekdayLongNames, monthLongNames,
} from './dates'
import { CalendarSelect, ColorSwatches, Toggle } from './controls'
@@ -30,20 +31,14 @@ const freqLabels: Record = {
YEARLY: 'Yearly',
}
-const weekdayShort = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-const weekdayLong = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
-const monthNames = [
- 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December',
-]
/** „Am 4." bzw. „Am 1. Freitag" – beide aus dem Startdatum abgeleitet, wie
* man es aus anderen Kalendern kennt. Bei jährlich kommt der Monat dazu. */
function monthlyModeLabels(start: Date, yearly: boolean): { day: string; weekday: string } {
const n = nthOfMonth(start)
const nth = n === -1 ? 'letzten' : `${n}.`
- const wd = weekdayLong[(start.getDay() + 6) % 7]
- const month = monthNames[start.getMonth()]
+ const wd = weekdayLongNames[(start.getDay() + 6) % 7]
+ const month = monthLongNames[start.getMonth()]
return yearly
? { day: `Am ${start.getDate()}. ${month}`, weekday: `Am ${nth} ${wd} im ${month}` }
: { day: `Am ${start.getDate()}.`, weekday: `Am ${nth} ${wd}` }
@@ -292,7 +287,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
)
})}
diff --git a/frontend/src/components/calendar/dates.test.ts b/frontend/src/components/calendar/dates.test.ts
new file mode 100644
index 0000000..0f85928
--- /dev/null
+++ b/frontend/src/components/calendar/dates.test.ts
@@ -0,0 +1,73 @@
+import { describe, it, expect } from 'vitest'
+import {
+ weekdayIndex, weekdayCodes,
+ weekdayShortNames, weekdayLongNames, weekdayLongNamesSun, monthLongNames,
+} from './dates'
+
+/**
+ * Die Namen kommen seit dem 20.09.2026 aus `Intl` statt aus sechs
+ * handgeschriebenen Arrays. Geprüft wird hier nicht, ob der Browser Englisch
+ * kann – sondern die **Reihenfolge**, denn daran hing die eigentliche Falle:
+ * Eine Liste begann am Montag, eine am Sonntag, jede passend zu ihrer
+ * Indizierung. Wer die falsche erwischt, verschiebt jeden Wochentag um eins.
+ */
+describe('Namen von Wochentagen und Monaten', () => {
+ it('beginnt die Woche am Montag – wie weekdayIndex', () => {
+ expect(weekdayLongNames[0]).toBe('Monday')
+ expect(weekdayLongNames[6]).toBe('Sunday')
+ expect(weekdayShortNames[0]).toBe('Mon')
+ expect(weekdayShortNames).toHaveLength(7)
+
+ // Der Beweis, dass Namen und Index zusammenpassen: Für jeden Tag einer
+ // echten Woche muss der Name am errechneten Index stehen.
+ const montag = new Date(2024, 0, 1)
+ for (let i = 0; i < 7; i++) {
+ const tag = new Date(2024, 0, 1 + i)
+ expect(weekdayIndex(tag)).toBe(i)
+ expect(weekdayLongNames[weekdayIndex(tag)]).toBe(
+ new Intl.DateTimeFormat('en', { weekday: 'long' }).format(tag))
+ }
+ expect(weekdayIndex(montag)).toBe(0)
+ })
+
+ it('hat eine zweite Liste, die am Sonntag beginnt – für getDay()', () => {
+ expect(weekdayLongNamesSun[0]).toBe('Sunday')
+ expect(weekdayLongNamesSun[1]).toBe('Monday')
+
+ // Genau dafür ist sie da: direkt mit getDay() indizierbar.
+ for (let i = 0; i < 7; i++) {
+ const tag = new Date(2024, 0, 1 + i)
+ expect(weekdayLongNamesSun[tag.getDay()]).toBe(
+ new Intl.DateTimeFormat('en', { weekday: 'long' }).format(tag))
+ }
+ })
+
+ it('passt zu den Wiederholungs-Kürzeln der Terminreihe', () => {
+ // weekdayCodes ist MO..SU. Stünden die Namen anders herum, bekäme die
+ // Auswahl „An diesen Tagen" die falsche Beschriftung – und ein
+ // Serientermin landete auf dem falschen Wochentag.
+ expect(weekdayCodes).toHaveLength(weekdayLongNames.length)
+ expect(weekdayCodes[0]).toBe('MO')
+ expect(weekdayLongNames[0].startsWith('Mo')).toBe(true)
+ expect(weekdayCodes[6]).toBe('SU')
+ expect(weekdayLongNames[6].startsWith('Su')).toBe(true)
+ })
+
+ it('liefert zwölf Monate, Januar zuerst', () => {
+ expect(monthLongNames).toHaveLength(12)
+ expect(monthLongNames[0]).toBe('January')
+ expect(monthLongNames[11]).toBe('December')
+ // Mitte des Monats als Bezugspunkt: Am Monatsersten könnte eine Zeitzone
+ // westlich von UTC in den Vormonat kippen.
+ for (let i = 0; i < 12; i++) {
+ expect(monthLongNames[new Date(2024, i, 15).getMonth()]).toBe(monthLongNames[i])
+ }
+ })
+
+ it('spricht Englisch, unabhängig von der Sprache des Browsers', () => {
+ // Die Oberfläche ist englisch; die Daten müssen es auch sein. Sonst stünde
+ // auf einem deutschen Rechner „Montag" neben „Repeat".
+ expect(monthLongNames[2]).toBe('March')
+ expect(weekdayLongNames[2]).toBe('Wednesday')
+ })
+})
diff --git a/frontend/src/components/calendar/dates.ts b/frontend/src/components/calendar/dates.ts
index 399ecc3..6b7f207 100644
--- a/frontend/src/components/calendar/dates.ts
+++ b/frontend/src/components/calendar/dates.ts
@@ -132,3 +132,57 @@ export function spannedDays(start: Date, end: Date | null, allDay: boolean): Dat
for (let d = first, i = 0; +d <= +last && i < 400; d = addDays(d, 1), i++) out.push(d)
return out
}
+
+// ── Namen von Wochentagen und Monaten ────────────────────────────────────────
+//
+// Bis zum 20.09.2026 standen die als **sechs** handgeschriebene Arrays in drei
+// Dateien – und `Intl` kam im ganzen Projekt nicht vor, obwohl jeder Browser
+// es seit Jahren mitbringt.
+//
+// Die Doppelpflege war nicht nur lästig, sie war eine Falle: `DayDialog`
+// begann seine Woche am Sonntag, die anderen am Montag, jeweils passend zu
+// ihrer Indizierung. Beide waren für sich richtig. Wer aber eines der Arrays
+// in eine neue Ansicht kopiert hätte, hätte sich mit hoher Wahrscheinlichkeit
+// einen Off-by-one gebaut, der nur an sieben Tagen im Jahr auffällt.
+//
+// Hier gibt es deshalb nur noch **eine** Ordnung: Montag zuerst, wie
+// `weekdayIndex` oben. Wer den Sonntag vorn braucht, nimmt `weekdayNamesSun`.
+
+/**
+ * **Feste Sprache, nicht die des Browsers.**
+ *
+ * `Intl.DateTimeFormat(undefined, …)` nähme die Spracheinstellung des
+ * Browsers – und läge auf einem deutschen Rechner „Montag" neben ein
+ * englisches „Repeat". Genau diesen Mischmasch haben wir am 18.09.2026
+ * ausgeräumt; ihn über die Hintertür wieder hereinzulassen wäre absurd.
+ *
+ * Die Oberfläche spricht Englisch, also sprechen die Daten Englisch. Kommt
+ * eines Tages eine Sprachwahl (siehe IDEAS.md), ist **diese Zeile** die
+ * Stelle, an der sie andockt – und dann stimmt es überall auf einmal.
+ */
+const uiLocale = 'en'
+
+/** Einmal rechnen, nicht bei jedem Rendern. */
+function bauen(anzahl: number, tag: (i: number) => Date, opts: Intl.DateTimeFormatOptions) {
+ const fmt = new Intl.DateTimeFormat(uiLocale, opts)
+ return Array.from({ length: anzahl }, (_, i) => fmt.format(tag(i)))
+}
+
+// Der 1. Januar 2024 war ein Montag – der Bezugspunkt für die Wochentage.
+const ersterMontag = (i: number) => new Date(2024, 0, 1 + i)
+// Mitte des Monats, damit keine Zeitzone den Monat kippen kann.
+const monatsMitte = (i: number) => new Date(2024, i, 15)
+
+/** Mo–So, kurz („Mon"). */
+export const weekdayShortNames = bauen(7, ersterMontag, { weekday: 'short' })
+/** Montag–Sonntag, lang. */
+export const weekdayLongNames = bauen(7, ersterMontag, { weekday: 'long' })
+/** Januar–Dezember, lang. */
+export const monthLongNames = bauen(12, monatsMitte, { month: 'long' })
+
+/**
+ * Dieselben Namen, aber **Sonntag zuerst** – für alles, was direkt mit
+ * `Date.getDay()` indiziert. Bewusst eine eigene Liste statt eines Umrechnens
+ * am Aufrufort: Genau dort entsteht sonst der Off-by-one.
+ */
+export const weekdayLongNamesSun = [weekdayLongNames[6], ...weekdayLongNames.slice(0, 6)]
diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx
index 7641328..6a52f73 100644
--- a/frontend/src/pages/CalendarPage.tsx
+++ b/frontend/src/pages/CalendarPage.tsx
@@ -14,6 +14,7 @@ import DayDialog, { type DayEvent } from '../components/calendar/DayDialog'
import DeleteEventDialog from '../components/calendar/DeleteEventDialog'
import {
addDays, dayKey, eventDay, isSameDay, spannedDays, startOfDay, startOfMonth, startOfWeek,
+ weekdayShortNames, monthLongNames,
} from '../components/calendar/dates'
import { useEventDrag } from '../components/calendar/useEventDrag'
import { TrashIcon, RenameIcon, DownloadIcon, PeopleIcon } from '../components/icons'
@@ -23,11 +24,6 @@ import EmptyState from '../components/EmptyState'
type ViewMode = 'month' | 'week' | 'list'
-const weekdayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-const monthLabels = [
- 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December',
-]
/** Höhe einer Stunde im Wochenraster. */
const HOUR_H = 44
@@ -332,7 +328,7 @@ export default function CalendarPage() {
}
const periodLabel = viewMode === 'month'
- ? `${monthLabels[cursor.getMonth()]} ${cursor.getFullYear()}`
+ ? `${monthLongNames[cursor.getMonth()]} ${cursor.getFullYear()}`
: viewMode === 'week'
? (() => {
const s = startOfWeek(cursor)
@@ -729,7 +725,7 @@ function MonthGrid({ cursor, byDay, onOpenDay, onEventClick, onEventDown }: {
return (