feat(format): makeFormatters accepts user timezone override

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).
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 11:55:58 +02:00
1 parent 4cb8ba3491
commit 3c608520f6
2 files changed
+33 -5

No files matched your search

+21
View File
@@ -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/);
});
});
+12 -5
View File
@@ -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) =>