diff --git a/components/calendar/__tests__/event-popover-duration-ux-p2-pim.test.tsx b/components/calendar/__tests__/event-popover-duration-ux-p2-pim.test.tsx new file mode 100644 index 000000000..8968975ef --- /dev/null +++ b/components/calendar/__tests__/event-popover-duration-ux-p2-pim.test.tsx @@ -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) => (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( + , + ); +} + +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(); + }); +}); diff --git a/components/calendar/event-detail-popover.tsx b/components/calendar/event-detail-popover.tsx index a9c6529b0..b6cff7a0b 100644 --- a/components/calendar/event-detail-popover.tsx +++ b/components/calendar/event-detail-popover.tsx @@ -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, locale: string): string | null { @@ -506,7 +514,7 @@ export function EventDetailPopover({
- ({formatDurationDisplay(durationMinutes)}) + ({formatDurationDisplay(durationMinutes, locale)})
{zoned && zonedText && (
@@ -529,7 +537,7 @@ export function EventDetailPopover({ ) : (
{formatTime(startDate)} – {formatTime(endDate)} - ({formatDurationDisplay(durationMinutes)}) + ({formatDurationDisplay(durationMinutes, locale)})
)}