fix: write an event's length in the app's language in its popover, as 1 Std. in German

This commit is contained in:
Linus Rath
2026-11-04 13:47:50 +01:00
parent 2c8c58ce08
commit f80f2ec612
2 changed files with 69 additions and 6 deletions
@@ -0,0 +1,55 @@
import { render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useSettingsStore } from '@/stores/settings-store';
import { AUTO_TIME_ZONE } from '@/lib/timezone';
import type { Calendar, CalendarEvent } from '@/lib/jmap/types';
// pim-p2-19: the German popover read "16:00 – 17:00 (1h)"; German writes "1 Std.".
const lang = vi.hoisted(() => ({ locale: 'de' }));
vi.mock('next-intl', () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => (values ? `${key}:${JSON.stringify(values)}` : key),
useLocale: () => lang.locale,
useFormatter: () => ({ dateTime: (d: Date | string) => String(d), relativeTime: (d: Date | string) => String(d), number: (n: number) => String(n) }),
}));
import { EventDetailPopover } from '../event-detail-popover';
const calendar = { id: 'cal', name: 'Work', myRights: { mayWriteAll: true, mayWriteOwn: true } } as unknown as Calendar;
function popover(duration: string) {
const event = {
id: 'e1', calendarIds: { cal: true }, title: 'Team sync', start: '2026-11-05T16:00:00', duration,
timeZone: 'Europe/Berlin', showWithoutTime: false, participants: null, alerts: null, useDefaultAlerts: false,
} as unknown as CalendarEvent;
render(
<EventDetailPopover event={event} calendar={calendar} anchorRect={new DOMRect(100, 100, 50, 20)}
onEdit={vi.fn()} onDelete={vi.fn()} onDuplicate={vi.fn()} onClose={vi.fn()} onSaveNote={vi.fn()}
currentUserEmails={['me@example.com']} />,
);
}
beforeEach(() => useSettingsStore.setState({ timeZone: 'Europe/Berlin' }));
afterEach(() => {
useSettingsStore.setState({ timeZone: AUTO_TIME_ZONE });
lang.locale = 'de';
});
describe('the popover writes the duration in the app language', () => {
it('in German', () => {
popover('PT1H');
expect(screen.getByText('(1 Std.)')).toBeInTheDocument();
expect(screen.queryByText('(1h)')).toBeNull();
});
it('with hours and minutes', () => {
popover('PT1H30M');
expect(screen.getByText('(1 Std. 30 Min.)')).toBeInTheDocument();
});
it('in English', () => {
lang.locale = 'en';
popover('PT45M');
expect(screen.getByText('(45 min)')).toBeInTheDocument();
});
});
+14 -6
View File
@@ -114,12 +114,20 @@ function computePosition(
};
}
function formatDurationDisplay(minutes: number): string {
if (minutes < 60) return `${minutes}min`;
/** "1 hr 30 min", "1 Std. 30 Min.": the language's own short units (pim-p2-19). */
export function formatDurationDisplay(minutes: number, locale: string): string {
const unit = (value: number, name: "hour" | "minute") => {
try {
return new Intl.NumberFormat(locale, { style: "unit", unit: name, unitDisplay: "short" }).format(value);
} catch {
return `${value}${name === "hour" ? "h" : "min"}`;
}
};
const h = Math.floor(minutes / 60);
const m = minutes % 60;
if (m === 0) return `${h}h`;
return `${h}h${m}min`;
if (h === 0) return unit(m, "minute");
if (m === 0) return unit(h, "hour");
return `${unit(h, "hour")} ${unit(m, "minute")}`;
}
function getRecurrenceLabel(event: CalendarEvent, t: ReturnType<typeof useTranslations>, locale: string): string | null {
@@ -506,7 +514,7 @@ export function EventDetailPopover({
</span>
</div>
<div className="text-muted-foreground text-xs">
({formatDurationDisplay(durationMinutes)})
({formatDurationDisplay(durationMinutes, locale)})
</div>
{zoned && zonedText && (
<div className="text-muted-foreground text-xs" data-testid="event-zone-times">
@@ -529,7 +537,7 @@ export function EventDetailPopover({
) : (
<div className="text-muted-foreground">
<bdi dir="ltr">{formatTime(startDate)} – {formatTime(endDate)}</bdi>
<span className="ms-1.5 text-xs">({formatDurationDisplay(durationMinutes)})</span>
<span className="ms-1.5 text-xs">({formatDurationDisplay(durationMinutes, locale)})</span>
</div>
)}
</>