feat(calendar): Oberfläche auf Englisch, Standardkalender heißt „Personal"

Die Kalenderseite war durchgehend deutsch, die übrige Oberfläche englisch –
80 Stellen in sechs Dateien. Kommentare bleiben deutsch; das Ersetzen hat
Kommentarzeilen übersprungen.

Shell, TagChip und UserMenu standen nur wegen ihrer Kommentare auf der
Verdachtsliste, deren Oberfläche war längst englisch.

Der automatisch angelegte Kalender hieß „Persönlich" und heißt jetzt
„Personal". Migration 000027 zieht die bestehenden nach, bewusst eng gefasst:
nur Zeilen mit Name *und* Farbe des Automaten. Wer sich selbst einen Kalender
„Persönlich" angelegt hat, behält seinen Namen – das ist seine Beschriftung,
nicht unsere. Nachgeprüft: Die API meldet danach 'Personal', nicht nur
„Migration gelaufen".

Dazu zwei Punkte aus Marcs Rückmeldung:

fix(ui): Der Leerzustand der Freigabe-Links stand auf anderer Höhe als die
übrigen – die Seite hatte ich beim Umbau übersehen. Nutzt jetzt EmptyState
wie alle anderen.

fix(ui): Beim Durchklicken von Ordner zu Ordner blitzte für einen Moment ein
Feld grauer Kästen auf. Sie sahen aus wie Dateien, die gleich wieder
verschwinden – „Geister". Der Platzhalter kommt jetzt erst nach 220 ms; ist
die Liste vorher da, wird er nie sichtbar, und solange bleibt die vorherige
stehen (load() löscht sie nicht). Nur beim allerersten Laden kommt er sofort:
Da gibt es nichts, was stehenbleiben könnte.

Nebenbei: Ein fehlgeschlagenes Laden eines Ordners ist nicht mehr stumm.
Vorher sah es aus wie ein Ordner, der zufällig denselben Inhalt hat wie der
vorige.

Geprüft: go build/vet sauber, tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen
der sieben Selbsttest-Suiten grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-18 18:38:30 +02:00
1 parent 1d10c6bd0e
commit 0591a40d12
14 files changed
+179 -101

No files matched your search

@@ -34,7 +34,7 @@ export default function CalendarDialog({ calendar, onSave, onCancel }: Props) {
borderRadius: 14, padding: 24, width: 340, maxWidth: '100%',
}}>
<h2 style={{ margin: '0 0 16px', fontSize: 16, fontWeight: 600, color: 'var(--color-text)' }}>
{calendar ? 'Kalender bearbeiten' : 'Neuer Kalender'}
{calendar ? 'Edit calendar' : 'New calendar'}
</h2>
<label style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 4, display: 'block' }}>Name</label>
@@ -48,7 +48,7 @@ export default function CalendarDialog({ calendar, onSave, onCancel }: Props) {
}}
/>
<label style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 8, display: 'block' }}>Farbe</label>
<label style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 8, display: 'block' }}>Color</label>
<div style={{ marginBottom: 20 }}>
<ColorSwatches value={color} onChange={setColor} />
</div>
@@ -2,10 +2,10 @@ import { useEffect } from 'react'
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss'
import type { CalendarEvent } from '../../api/client'
const weekdayLong = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
const weekdayLong = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const monthLabels = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
]
export interface DayEvent extends CalendarEvent {
@@ -42,7 +42,7 @@ export default function DayDialog({ day, events, onPick, onCreate, onClose }: {
}, [onClose])
const timeLabel = (ev: DayEvent) => {
if (ev.all_day) return 'ganztägig'
if (ev.all_day) return 'all day'
const from = ev.startsAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
if (!ev.endsAt) return from
const to = ev.endsAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
@@ -86,7 +86,7 @@ export default function DayDialog({ day, events, onPick, onCreate, onClose }: {
<span style={{ width: 4, alignSelf: 'stretch', borderRadius: 2, background: ev.color, flexShrink: 0 }} />
<span style={{ flex: 1, minWidth: 0 }}>
<span style={{ display: 'block', fontSize: 13.5, color: 'var(--color-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{ev.summary || '(ohne Titel)'}
{ev.summary || '(untitled)'}
{ev.recurring && <span style={{ marginLeft: 6, fontSize: 11, color: 'var(--color-text-muted)' }}>↻</span>}
</span>
<span style={{ display: 'block', fontSize: 11.5, color: 'var(--color-text-muted)' }}>
@@ -42,17 +42,17 @@ export default function DeleteEventDialog({ summary, recurring, onSingle, onSeri
borderRadius: 14, padding: 24, width: 380, maxWidth: '100%',
}}>
<h2 style={{ margin: '0 0 8px', fontSize: 16, fontWeight: 600, color: 'var(--color-text)' }}>
{recurring ? 'Serientermin löschen' : 'Termin löschen'}
{recurring ? 'Delete recurring event' : 'Delete event'}
</h2>
<p style={{ margin: '0 0 20px', fontSize: 13.5, color: 'var(--color-text-muted)', lineHeight: 1.5 }}>
{recurring
? <>„{summary || 'Ohne Titel'}" gehört zu einer Reihe. Was soll gelöscht werden?</>
: <>„{summary || 'Ohne Titel'}" unwiderruflich löschen?</>}
? <>„{summary || 'Untitled'}" gehört zu einer Reihe. Was soll gelöscht werden?</>
: <>„{summary || 'Untitled'}" unwiderruflich löschen?</>}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<button onClick={onSingle} style={danger}>
{recurring ? 'Nur diesen Termin' : 'Löschen'}
{recurring ? 'Only this event' : 'Delete'}
</button>
{recurring && (
<button onClick={onSeries} style={danger}>
@@ -23,18 +23,18 @@ type FreqChoice = 'NONE' | Recurrence['freq']
type EndChoice = 'NEVER' | 'COUNT' | 'UNTIL'
const freqLabels: Record<FreqChoice, string> = {
NONE: 'Einmalig',
DAILY: 'Täglich',
WEEKLY: 'Wöchentlich',
MONTHLY: 'Monatlich',
YEARLY: 'Jährlich',
NONE: 'Does not repeat',
DAILY: 'Daily',
WEEKLY: 'Weekly',
MONTHLY: 'Monthly',
YEARLY: 'Yearly',
}
const weekdayShort = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
const weekdayLong = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
const weekdayShort = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const weekdayLong = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
const monthNames = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
]
/** „Am 4." bzw. „Am 1. Freitag" – beide aus dem Startdatum abgeleitet, wie
@@ -185,7 +185,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
borderRadius: 14, padding: 24, width: 440, maxWidth: '100%', maxHeight: '90vh', overflowY: 'auto',
}}>
<h2 style={{ margin: '0 0 16px', fontSize: 16, fontWeight: 600, color: 'var(--color-text)' }}>
{event ? (isSeries ? 'Terminreihe bearbeiten' : 'Termin bearbeiten') : 'Neuer Termin'}
{event ? (isSeries ? 'Edit series' : 'Edit event') : 'New event'}
</h2>
{readOnly && (
@@ -197,20 +197,20 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
)}
{isSeries && !readOnly && (
<div style={noteStyle}>
Terminreihe: Änderungen gelten für <strong>alle</strong> Termine der
Terminreihe: Änderungen gelten für <strong>every</strong> Termine der
Reihe. Angezeigt wird deshalb ihr Beginn, nicht der angeklickte Tag.
</div>
)}
<div style={{ marginBottom: 12 }}>
<label style={labelStyle}>Titel</label>
<label style={labelStyle}>Title</label>
<input ref={titleRef} type="text" value={summary} disabled={readOnly}
onChange={e => setSummary(e.target.value)} style={inputStyle} required />
</div>
{calendars.length > 1 && (
<div style={{ marginBottom: 12 }}>
<label style={labelStyle}>Kalender</label>
<label style={labelStyle}>Calendar</label>
{/* Auch beim Bearbeiten wählbar: Der Termin wechselt dann beim
Speichern die Sammlung, behält aber seine Kennung – für
CalDAV-Clients derselbe Termin an neuer Adresse, nicht ein
@@ -219,7 +219,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
{!!event && calId !== calendarId && (
<p style={{ margin: '6px 2px 0', fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>
Wird beim Speichern nach „{calendars.find(c => c.id === calId)?.name}" verschoben
{isSeries ? ' – mit der ganzen Reihe.' : '.'}
{isSeries ? ' – together with the whole series.' : '.'}
</p>
)}
</div>
@@ -237,7 +237,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
</div>
{!allDay && (
<div style={{ width: 116 }}>
<label style={labelStyle}>Uhrzeit</label>
<label style={labelStyle}>Time</label>
<input type="time" value={startTime} disabled={readOnly} onChange={e => setStartTime(e.target.value)} style={inputStyle} />
</div>
)}
@@ -245,13 +245,13 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
<div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
<div style={{ flex: 1 }}>
<label style={labelStyle}>Ende</label>
<label style={labelStyle}>End</label>
<input type="date" value={endDate} min={startDate} disabled={readOnly}
onChange={e => setEndDate(e.target.value)} style={inputStyle} />
</div>
{!allDay && (
<div style={{ width: 116 }}>
<label style={labelStyle}>Uhrzeit</label>
<label style={labelStyle}>Time</label>
<input type="time" value={endTime} disabled={readOnly} onChange={e => setEndTime(e.target.value)} style={inputStyle} />
</div>
)}
@@ -259,7 +259,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
{/* ── Wiederholung ───────────────────────────────────────────── */}
<div style={{ marginBottom: 14 }}>
<label style={labelStyle}>Wiederholung</label>
<label style={labelStyle}>Repeat</label>
<div style={{ display: 'flex', gap: 8 }}>
<select value={freq} disabled={readOnly}
onChange={e => setFreq(e.target.value as FreqChoice)}
@@ -270,7 +270,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
</select>
{freq !== 'NONE' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
<span style={{ fontSize: 12.5, color: 'var(--color-text-muted)' }}>alle</span>
<span style={{ fontSize: 12.5, color: 'var(--color-text-muted)' }}>every</span>
<input type="number" min={1} max={99} value={interval} disabled={readOnly}
onChange={e => setInterval(e.target.value)}
style={{ ...inputStyle, width: 60 }} />
@@ -283,7 +283,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
eine Reihe, die ihren eigenen ersten Termin auslässt, ist keine. */}
{freq === 'WEEKLY' && (
<div style={{ marginBottom: 14 }}>
<label style={labelStyle}>An diesen Tagen</label>
<label style={labelStyle}>On these days</label>
<div style={{ display: 'flex', gap: 5 }}>
{weekdayCodes.map((code, i) => {
const on = activeDays.includes(code)
@@ -314,7 +314,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
{/* Monatlich/jährlich: am festen Tag oder am n-ten Wochentag. */}
{(freq === 'MONTHLY' || freq === 'YEARLY') && (
<div style={{ marginBottom: 14 }}>
<label style={labelStyle}>Wiederholt sich</label>
<label style={labelStyle}>Repeats</label>
<select value={monthMode} disabled={readOnly}
onChange={e => setMonthMode(e.target.value as 'DAY' | 'WEEKDAY')}
style={inputStyle}>
@@ -326,20 +326,20 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
{freq !== 'NONE' && (
<div style={{ marginBottom: 14 }}>
<label style={labelStyle}>Endet</label>
<label style={labelStyle}>Ends</label>
<div style={{ display: 'flex', gap: 8 }}>
<select value={endChoice} disabled={readOnly}
onChange={e => setEndChoice(e.target.value as EndChoice)}
style={{ ...inputStyle, flex: 1 }}>
<option value="NEVER">Nie</option>
<option value="COUNT">Nach Anzahl</option>
<option value="UNTIL">An einem Datum</option>
<option value="NEVER">Never</option>
<option value="COUNT">After</option>
<option value="UNTIL">On a date</option>
</select>
{endChoice === 'COUNT' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
<input type="number" min={1} max={999} value={count} disabled={readOnly}
onChange={e => setCount(e.target.value)} style={{ ...inputStyle, width: 70 }} />
<span style={{ fontSize: 12.5, color: 'var(--color-text-muted)' }}>Mal</span>
<span style={{ fontSize: 12.5, color: 'var(--color-text-muted)' }}>times</span>
</div>
)}
{endChoice === 'UNTIL' && (
@@ -352,17 +352,17 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
{/* ── Farbe ──────────────────────────────────────────────────── */}
<div style={{ marginBottom: 14 }}>
<label style={labelStyle}>Farbe</label>
<label style={labelStyle}>Color</label>
<ColorSwatches value={color} onChange={setColor} inherit={calColor} disabled={readOnly} />
</div>
<div style={{ marginBottom: 12 }}>
<label style={labelStyle}>Ort</label>
<label style={labelStyle}>Location</label>
<input type="text" value={location} disabled={readOnly} onChange={e => setLocation(e.target.value)} style={inputStyle} />
</div>
<div style={{ marginBottom: 20 }}>
<label style={labelStyle}>Beschreibung</label>
<label style={labelStyle}>Description</label>
<textarea value={description} disabled={readOnly} onChange={e => setDescription(e.target.value)}
rows={3} style={{ ...inputStyle, resize: 'vertical' }} />
</div>
@@ -384,7 +384,7 @@ export default function EventDialog({ event, defaultStart, calendars, calendarId
padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)',
background: 'transparent', color: 'var(--color-text-muted)', fontSize: 13, cursor: 'pointer',
}}>
{readOnly ? 'Schließen' : 'Abbrechen'}
{readOnly ? 'Close' : 'Cancel'}
</button>
{!readOnly && (
<button type="submit" style={{
@@ -85,7 +85,7 @@ export function ColorSwatches({ value, onChange, inherit, disabled }: {
return (
<div style={{ display: 'flex', gap: 9, flexWrap: 'wrap', alignItems: 'center' }}>
{inherit !== undefined && dot(inherit, value === '', () => onChange(''), 'inherit', 'Farbe des Kalenders')}
{inherit !== undefined && dot(inherit, value === '', () => onChange(''), 'inherit', 'Calendar color')}
{inherit !== undefined && (
<span style={{ width: 1, height: 20, background: 'var(--color-border)', margin: '0 2px' }} />
)}
@@ -151,7 +151,7 @@ export function CalendarSelect({ calendars, value, onChange, disabled }: {
aria-haspopup="listbox" aria-expanded={open} style={field}>
{current && dot(current.color, true)}
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{current?.name ?? 'Kalender wählen'}
{current?.name ?? 'Choose calendar'}
</span>
<span style={{ color: 'var(--color-text-muted)', fontSize: 10, flexShrink: 0 }}>▼</span>
</button>