From 3c608520f6558764773ca67d2f6c8e86d8a7d89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 11:55:58 +0200 Subject: [PATCH] feat(format): makeFormatters accepts user timezone override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.4.25 W7 phase 3.2 — extends the shared locale formatter to accept an optional second argument carrying the per-user display timezone. makeFormatters(locale) — unchanged, still renders in Europe/Berlin. makeFormatters(locale, userTz) — renders in the user's zone. The empty-string and undefined cases both fall back to DISPLAY_TIMEZONE so admin tools, the legacy client wrapper in src/lib/format.ts, and audit-log viewers keep behaving identically. Three tests cover Asia/Tokyo wall-clock + date roll-over and the empty-string fallback. No production callers yet — wire-up happens in the per-surface commits that follow (doctor-report PDF, Coach snapshot, chart axes). --- src/lib/__tests__/format-locale.test.ts | 21 +++++++++++++++++++++ src/lib/format-locale.ts | 17 ++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/lib/__tests__/format-locale.test.ts b/src/lib/__tests__/format-locale.test.ts index 91c6b941f..cbfe7705b 100644 --- a/src/lib/__tests__/format-locale.test.ts +++ b/src/lib/__tests__/format-locale.test.ts @@ -61,4 +61,25 @@ describe("makeFormatters", () => { expect(deFmt.time(sample)).toBe("16:30"); expect(enFmt.time(sample)).toBe("16:30"); }); + + // v1.4.25 W7 — formatters accept a per-user timezone override. + it("renders time in the user's tz when userTz is passed", () => { + const tokyo = makeFormatters("en", "Asia/Tokyo"); + // 14:30 UTC = 23:30 Tokyo + expect(tokyo.time(sample)).toBe("23:30"); + }); + + it("renders time in Europe/Berlin when userTz is empty", () => { + const fallback = makeFormatters("en", ""); + expect(fallback.time(sample)).toBe("16:30"); + }); + + it("renders date in the user's tz when userTz is passed", () => { + // 23:30 UTC on May 10 = 01:30 May 11 Tokyo + const lateUtc = new Date("2026-05-10T23:30:00Z"); + const tokyo = makeFormatters("en", "Asia/Tokyo"); + expect(tokyo.date(lateUtc)).toMatch(/2026/); + // The narrow check: day-of-month should be 11, not 10. + expect(tokyo.date(lateUtc)).toMatch(/11/); + }); }); diff --git a/src/lib/format-locale.ts b/src/lib/format-locale.ts index 84f16e78b..2a72e3f23 100644 --- a/src/lib/format-locale.ts +++ b/src/lib/format-locale.ts @@ -7,14 +7,21 @@ * * Call via the `useFormatters()` client hook. For server-only code paths * (jobs, API routes, PDF generation) pass the locale explicitly through - * `makeFormatters(locale)`. + * `makeFormatters(locale)`. The optional second argument carries the + * per-user timezone (v1.4.25 W7) — when omitted the formatter falls back + * to `DISPLAY_TIMEZONE` so legacy callers keep rendering in Europe/Berlin. * - * Timezone is fixed to Europe/Berlin for display (user expectation). UTC is - * preserved in the database. + * UTC is always preserved in the database. */ import type { Locale } from "./i18n/config"; +/** + * Fallback timezone for surfaces that are not yet user-scoped (admin + * tables, audit log viewer, anything without a resolved user). User- + * scoped surfaces should call `makeFormatters(locale, userTz)` with + * the value from `resolveUserTimezone()`. + */ export const DISPLAY_TIMEZONE = "Europe/Berlin"; /** @@ -58,9 +65,9 @@ export interface Formatters { monthShort: (value: DateInput) => string; } -export function makeFormatters(locale: Locale): Formatters { +export function makeFormatters(locale: Locale, userTz?: string): Formatters { const intlLocale = resolveIntlLocale(locale); - const tz = DISPLAY_TIMEZONE; + const tz = userTz && userTz.length > 0 ? userTz : DISPLAY_TIMEZONE; return { number: (value, fractionDigits) =>