119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
/**
|
|
* 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();
|
|
* <span>{formatDateTime(req.createdAt, f)}</span>
|
|
* <span title={formatDateTime(req.createdAt, f)}>
|
|
* {formatRelative(req.createdAt, f)}
|
|
* </span>
|
|
*
|
|
* 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<typeof useFormatter>;
|
|
|
|
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 });
|
|
}
|