/** * Locale-aware date/time formatting helpers. * * Built on top of next-intl's format API, which wraps Intl.DateTimeFormat / * Intl.RelativeTimeFormat using the active request locale. These helpers add * three things on top of raw next-intl: * * 1. Tolerant input — accepts string | Date | null | undefined and returns * a stable em-dash for missing values, so call sites don't need to * conditionally render. * 2. Two presets used everywhere in the portal (`dateTime`, `dateOnly`) * so the four locales render consistently. German/French/Italian use * 24h DD.MM.YYYY HH:mm; English uses 12h MMM D, YYYY h:mm a. * 3. A `relative` helper that auto-picks the right unit (minute/hour/day/ * week/month) based on the elapsed delta. * * Usage in client components: * * import { useFormatter } from "next-intl"; * import { formatDateTime, formatRelative } from "@/lib/format"; * * const f = useFormatter(); * {formatDateTime(req.createdAt, f)} * * {formatRelative(req.createdAt, f)} * * * Usage in server components: * * import { getFormatter } from "next-intl/server"; * const f = await getFormatter(); * ...same calls... */ // next-intl's `useFormatter()` (client) and `getFormatter()` (server) return // the same shape. We derive the type from useFormatter's return so we stay // in sync with next-intl version bumps without hand-maintaining a mirror. import type { useFormatter } from "next-intl"; type Formatter = ReturnType; const FALLBACK = "—"; function toDate(value: string | Date | null | undefined): Date | null { if (!value) return null; if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value; const d = new Date(value); return Number.isNaN(d.getTime()) ? null : d; } /** * Full date+time, locale-formatted. Returns "—" if the value is missing. * * de: 25.04.2026, 14:30 * en: Apr 25, 2026, 2:30 PM * fr: 25 avr. 2026, 14:30 * it: 25 apr 2026, 14:30 */ export function formatDateTime( value: string | Date | null | undefined, formatter: Formatter ): string { const d = toDate(value); if (!d) return FALLBACK; return formatter.dateTime(d, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } /** * Date only, locale-formatted. Use in dense table cells. */ export function formatDateOnly( value: string | Date | null | undefined, formatter: Formatter ): string { const d = toDate(value); if (!d) return FALLBACK; return formatter.dateTime(d, { year: "numeric", month: "short", day: "numeric", }); } /** * Relative time ("2 hours ago", "vor 2 Stunden", etc.). * Picks the unit automatically based on the magnitude of the delta. * Returns "—" if the value is missing. * * Anchors against `now` (defaults to current time) so SSR and client * render the same string when called within a single request. */ export function formatRelative( value: string | Date | null | undefined, formatter: Formatter, now: Date = new Date() ): string { const d = toDate(value); if (!d) return FALLBACK; const diffMs = d.getTime() - now.getTime(); const absSeconds = Math.abs(diffMs) / 1000; let unit: Intl.RelativeTimeFormatUnit; if (absSeconds < 60) unit = "second"; else if (absSeconds < 3_600) unit = "minute"; else if (absSeconds < 86_400) unit = "hour"; else if (absSeconds < 604_800) unit = "day"; else if (absSeconds < 2_592_000) unit = "week"; else if (absSeconds < 31_536_000) unit = "month"; else unit = "year"; return formatter.relativeTime(d, { now, unit }); }