diff --git a/web/src/lib/format-timestamp.test.ts b/web/src/lib/format-timestamp.test.ts new file mode 100644 index 0000000..a940430 --- /dev/null +++ b/web/src/lib/format-timestamp.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "vitest"; + +import { formatTimestamp } from "./format-timestamp"; + +test("formats a UTC timestamp with date and time in the given locale", () => { + const out = formatTimestamp("2026-06-08T12:30:00Z", "UTC", "en"); + expect(out).toContain("2026"); + expect(out).toContain("12:30"); +}); + +test("applies the timezone — a near-midnight UTC instant shifts the calendar day", () => { + // 02:00 UTC on Jun 8 is 22:00 on Jun 7 in New York (EDT, UTC-4) + const ny = formatTimestamp("2026-06-08T02:00:00Z", "America/New_York", "en"); + const utc = formatTimestamp("2026-06-08T02:00:00Z", "UTC", "en"); + expect(ny).toContain("Jun 7"); + expect(utc).toContain("Jun 8"); +}); + +test("an invalid IANA zone does not throw and falls back to UTC", () => { + const out = formatTimestamp("2026-06-08T12:30:00Z", "Not/AZone", "en"); + expect(out).toContain("2026"); +}); + +test("null renders the em-dash placeholder; an unparseable string is returned unchanged", () => { + expect(formatTimestamp(null, "UTC", "en")).toBe("—"); + expect(formatTimestamp("not-a-date", "UTC", "en")).toBe("not-a-date"); +}); diff --git a/web/src/lib/format-timestamp.ts b/web/src/lib/format-timestamp.ts new file mode 100644 index 0000000..6adfbac --- /dev/null +++ b/web/src/lib/format-timestamp.ts @@ -0,0 +1,18 @@ +/** Formats a UTC ISO timestamp for display in the instance timezone + active locale. + * Storage/transmission stay UTC — this is display-only. Falls back to UTC formatting on an + * invalid IANA zone (a misconfigured instance) rather than throwing. */ +export function formatTimestamp(value: unknown, timeZone: string, locale: string): string { + if (typeof value !== "string") return value == null ? "—" : String(value); + + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + + const opts = { dateStyle: "medium", timeStyle: "short" } as const; + + try { + return new Intl.DateTimeFormat(locale, { ...opts, timeZone }).format(date); + } catch { + // Invalid IANA timeZone (misconfigured instance) — fall back to UTC rather than crash. + return new Intl.DateTimeFormat(locale, { ...opts, timeZone: "UTC" }).format(date); + } +}