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

+28 -31
View File
@@ -23,10 +23,10 @@ import EmptyState from '../components/EmptyState'
type ViewMode = 'month' | 'week' | 'list'
const weekdayLabels = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
const weekdayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
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',
]
/** Höhe einer Stunde im Wochenraster. */
@@ -157,7 +157,7 @@ export default function CalendarPage() {
// eine Ansicht sollte an einer unerwarteten Antwort nicht zerbrechen.
setCalendars(await calendarApi.list() ?? [])
} catch (e) {
setError(e instanceof APIError ? e.message : 'Laden fehlgeschlagen')
setError(e instanceof APIError ? e.message : 'Could not load the calendar')
} finally {
setLoading(false)
}
@@ -233,7 +233,7 @@ export default function CalendarPage() {
setEventDialog(null)
await refreshEvents()
} catch (e) {
setError(e instanceof APIError ? e.message : 'Speichern fehlgeschlagen')
setError(e instanceof APIError ? e.message : 'Could not save')
}
}
@@ -251,7 +251,7 @@ export default function CalendarPage() {
})
await refreshEvents()
} catch (e) {
setError(e instanceof APIError ? e.message : 'Verschieben fehlgeschlagen')
setError(e instanceof APIError ? e.message : 'Could not move the event')
}
}
@@ -276,7 +276,7 @@ export default function CalendarPage() {
setEventDialog(null)
await refreshEvents()
} catch (e) {
setError(e instanceof APIError ? e.message : 'Löschen fehlgeschlagen')
setError(e instanceof APIError ? e.message : 'Could not delete')
}
}
@@ -290,7 +290,7 @@ export default function CalendarPage() {
setCalDialog(null)
await loadCalendars()
} catch (e) {
setError(e instanceof APIError ? e.message : 'Speichern fehlgeschlagen')
setError(e instanceof APIError ? e.message : 'Could not save')
}
}
@@ -301,7 +301,7 @@ export default function CalendarPage() {
setDeleteCalendar(null)
await loadCalendars()
} catch (e) {
setError(e instanceof APIError ? e.message : 'Löschen fehlgeschlagen')
setError(e instanceof APIError ? e.message : 'Could not delete')
}
}
@@ -318,7 +318,7 @@ export default function CalendarPage() {
await calendarApi.import(calId, file)
await refreshEvents()
} catch (err) {
setError(err instanceof APIError ? err.message : 'Import fehlgeschlagen')
setError(err instanceof APIError ? err.message : 'Import failed')
}
}
@@ -338,19 +338,16 @@ export default function CalendarPage() {
const e = addDays(s, 6)
return `${s.getDate()}.${s.getMonth() + 1}. – ${e.getDate()}.${e.getMonth() + 1}.${e.getFullYear()}`
})()
: 'Alle Termine'
: 'All events'
if (loading) {
// Deutsch, wie der Rest dieser Seite. Sie fällt damit aus der sonst
// englischen Oberfläche heraus – das ist bekannt und gehört in einem
// Zug erledigt, nicht halb.
return <div className="aether-content"><SkeletonRows /></div>
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', minWidth: 0 }}>
<PageHeader
title="Kalender"
title="Calendar"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="5" width="18" height="16" rx="2" />
@@ -377,17 +374,17 @@ export default function CalendarPage() {
color: viewMode === m ? '#fff' : 'var(--color-text-muted)',
transition: 'background 0.15s, color 0.15s',
}}>
{m === 'month' ? 'Monat' : m === 'week' ? 'Woche' : 'Liste'}
{m === 'month' ? 'Month' : m === 'week' ? 'Week' : 'List'}
</button>
))}
</div>
{viewMode !== 'list' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<button onClick={() => stepPeriod(-1)} style={navBtnStyle} aria-label="zurück">‹</button>
<button onClick={() => stepPeriod(-1)} style={navBtnStyle} aria-label="back">‹</button>
<span style={{ fontSize: 13, color: 'var(--color-text)', minWidth: 140, textAlign: 'center' }}>{periodLabel}</span>
<button onClick={() => stepPeriod(1)} style={navBtnStyle} aria-label="weiter">›</button>
<button onClick={() => setCursor(new Date())} style={{ ...navBtnStyle, width: 'auto', padding: '0 10px', fontSize: 12 }}>Heute</button>
<button onClick={() => setCursor(new Date())} style={{ ...navBtnStyle, width: 'auto', padding: '0 10px', fontSize: 12 }}>Today</button>
</div>
)}
@@ -414,8 +411,8 @@ export default function CalendarPage() {
padding: 14, overflowY: 'auto',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<span style={{ fontSize: 11, fontWeight: 700, color: 'var(--color-text-muted)', letterSpacing: 0.5 }}>KALENDER</span>
<button onClick={() => setCalDialog({})} title="Neuer Kalender" style={{
<span style={{ fontSize: 11, fontWeight: 700, color: 'var(--color-text-muted)', letterSpacing: 0.5 }}>CALENDARS</span>
<button onClick={() => setCalDialog({})} title="New calendar" style={{
border: 'none', background: 'none', cursor: 'pointer', color: 'var(--color-primary)',
fontSize: 18, lineHeight: 1, padding: '0 2px',
}}>+</button>
@@ -449,7 +446,7 @@ export default function CalendarPage() {
<button
onClick={() => toggleVisible(cal.id)}
role="switch" aria-checked={on}
title={on ? 'Ausblenden' : 'Einblenden'}
title={on ? 'Hide' : 'Show'}
style={{
width: 15, height: 15, borderRadius: '50%', flexShrink: 0, padding: 0,
border: `2px solid ${cal.color}`,
@@ -466,7 +463,7 @@ export default function CalendarPage() {
nebeneinander wären sonst nicht zu unterscheiden. */}
{cal.shared && cal.owner_name && (
<span style={{ color: 'var(--color-text-muted)', fontSize: 11.5 }}>
{' · '}{cal.owner_name}{cal.permission === 'read' ? ' (nur lesen)' : ''}
{' · '}{cal.owner_name}{cal.permission === 'read' ? ' (read-only)' : ''}
</span>
)}
</span>
@@ -540,7 +537,7 @@ export default function CalendarPage() {
border: `1px solid ${drag.ev.color}`, boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{drag.ev.summary || '(ohne Titel)'}{drag.ev.recurring ? ' ↻ (ganze Reihe)' : ''}
{drag.ev.summary || '(untitled)'}{drag.ev.recurring ? ' ↻ (entire series)' : ''}
</div>
)}
@@ -586,7 +583,7 @@ export default function CalendarPage() {
{deleteCalendar && (
<ConfirmDialog
title="Kalender löschen"
title="Delete calendar"
message={`"${deleteCalendar.name}" mit allen Terminen darin unwiderruflich löschen?`}
confirmLabel="Löschen" danger
onConfirm={confirmDeleteCalendar}
@@ -707,7 +704,7 @@ function EventChip({ ev, onClick, onDown }: {
}}
>
{!ev.all_day && <span style={{ opacity: 0.75 }}>{timeLabel(ev.startsAt)}</span>}
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{ev.summary || '(ohne Titel)'}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{ev.summary || '(untitled)'}</span>
{ev.recurring && <span style={{ opacity: 0.7, flexShrink: 0 }}>↻</span>}
</div>
)
@@ -894,7 +891,7 @@ function WeekGrid({ cursor, byDay, onSlotClick, onOpenDay, onEventClick, onEvent
{/* Ganztägige Termine – eigene Zeile über dem Stundenraster. */}
<div style={{ display: 'grid', gridTemplateColumns: cols, borderBottom: '1px solid var(--color-border)', flexShrink: 0, minHeight: 26 }}>
<div style={{ fontSize: 9.5, color: 'var(--color-text-muted)', padding: '4px 6px', textAlign: 'right' }}>ganztg.</div>
<div style={{ fontSize: 9.5, color: 'var(--color-text-muted)', padding: '4px 6px', textAlign: 'right' }}>all-day</div>
{days.map(day => (
<div key={+day} className="aether-cal-cell" onClick={() => onOpenDay(day)}
style={{ borderLeft: '1px solid var(--color-border)', padding: 2, cursor: 'pointer', minWidth: 0 }}>
@@ -960,7 +957,7 @@ function WeekGrid({ cursor, byDay, onSlotClick, onOpenDay, onEventClick, onEvent
}}
>
<div style={{ fontSize: 10.5, color: ev.color, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{ev.summary || '(ohne Titel)'}{ev.recurring ? ' ↻' : ''}
{ev.summary || '(untitled)'}{ev.recurring ? ' ↻' : ''}
</div>
{height > 28 && (
<div style={{ fontSize: 9.5, color: 'var(--color-text-muted)' }}>{timeLabel(ev.startsAt)}</div>
@@ -999,8 +996,8 @@ function ListAgenda({ events, onEventClick, onEventDown }: {
return (
<EmptyState
icon="📅"
title="Keine Termine"
hint="In diesem Zeitraum steht nichts an."
title="No events"
hint="Nothing scheduled in this period."
/>
)
}
@@ -1015,7 +1012,7 @@ function ListAgenda({ events, onEventClick, onEventDown }: {
color: isSameDay(day, today) ? 'var(--color-primary)' : 'var(--color-text-muted)',
}}>
{weekdayLabels[(day.getDay() + 6) % 7]}, {day.getDate()}. {monthLabels[day.getMonth()]} {day.getFullYear()}
{isSameDay(day, today) && ' · heute'}
{isSameDay(day, today) && ' · today'}
</div>
{evs.map(ev => (
<div key={eventKey(ev)} className="aether-cal-cell"
@@ -1035,7 +1032,7 @@ function ListAgenda({ events, onEventClick, onEventDown }: {
{ev.all_day ? 'ganztg.' : timeLabel(ev.startsAt)}
</span>
<span style={{ fontSize: 13.5, color: 'var(--color-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{ev.summary || '(ohne Titel)'}{ev.recurring ? ' ↻' : ''}
{ev.summary || '(untitled)'}{ev.recurring ? ' ↻' : ''}
</span>
{ev.location && (
<span style={{ fontSize: 12, color: 'var(--color-text-muted)', marginLeft: 'auto', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>