From e1cbb705273a261d26b489f35a7ddca6567d9663 Mon Sep 17 00:00:00 2001 From: Igor Makowski <56691628+Mnigos@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:34:12 +0200 Subject: [PATCH] fix(web): the custom snooze calendar starts the week where the locale does (#12745) --- .../web/src/components/CustomSnoozeDialog.tsx | 2 ++ apps/web/src/timestampFormat.test.ts | 26 +++++++++++++++++ apps/web/src/timestampFormat.ts | 29 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/apps/web/src/components/CustomSnoozeDialog.tsx b/apps/web/src/components/CustomSnoozeDialog.tsx index d80ebf0d36fb9ed1cba2227d87ab47958a4544b4..453efb1cbe5367d132a8fa006158843e4805ecb8 100644 --- a/apps/web/src/components/CustomSnoozeDialog.tsx +++ b/apps/web/src/components/CustomSnoozeDialog.tsx @@ -10,6 +10,7 @@ import { import { Button } from "./ui/button"; import { CalendarIcon } from "lucide-react"; import { Calendar } from "./ui/calendar"; +import { weekStartsOn } from "../timestampFormat"; import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; @@ -149,6 +150,7 @@ function CustomSnoozeDialog() { required selected={date} defaultMonth={date} + {...(weekStartsOn === undefined ? {} : { weekStartsOn })} disabled={{ before: new Date(new Date().setHours(0, 0, 0, 0)) }} onSelect={(selected) => { setDate(selected); diff --git a/apps/web/src/timestampFormat.test.ts b/apps/web/src/timestampFormat.test.ts index 8c6287010d8f1929618f3b77cc4dcd1e25259f4e..d748853000c5c36e129f4ccca1bfc670d9a70d01 100644 --- a/apps/web/src/timestampFormat.test.ts +++ b/apps/web/src/timestampFormat.test.ts @@ -52,6 +52,32 @@ describe("formatShortTimestamp", () => { }); }); +describe("resolveWeekStartsOn", () => { + it.each([ + ["en-US", 0], + ["en-GB", 1], + ["pl-PL", 1], + ["ar-EG", 6], + ])("starts the %s week on weekday %i", async (locale, weekday) => { + const { resolveWeekStartsOn } = await import("./timestampFormat"); + expect(resolveWeekStartsOn(locale)).toBe(weekday); + }); + + it("leaves the default to the caller for a malformed locale", async () => { + const { resolveWeekStartsOn } = await import("./timestampFormat"); + expect(resolveWeekStartsOn("not a locale")).toBeUndefined(); + }); + + it("follows the locale the desktop host reports", async () => { + vi.stubGlobal("window", { desktopBridge: { getSystemLocale: () => "en-GB" } }); + vi.resetModules(); + const { weekStartsOn } = await import("./timestampFormat"); + expect(weekStartsOn).toBe(1); + vi.unstubAllGlobals(); + vi.resetModules(); + }); +}); + describe("formatChatTimestampTooltip", () => { afterEach(() => { vi.restoreAllMocks(); diff --git a/apps/web/src/timestampFormat.ts b/apps/web/src/timestampFormat.ts index 9dd463bb50fa396c3511d6d99e476c158c1bb452..5106a00bdf2f4938d24c01760313348ab0554d34 100644 --- a/apps/web/src/timestampFormat.ts +++ b/apps/web/src/timestampFormat.ts @@ -53,6 +53,35 @@ function readHostSystemLocale(): string | null { const timestampLocale = resolveTimestampLocale(readHostSystemLocale()); +const WEEKDAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const; +type WeekdayIndex = (typeof WEEKDAY_INDEXES)[number]; + +type LocaleWithWeekInfo = Intl.Locale & { + readonly weekInfo?: { readonly firstDay: number }; + getWeekInfo?: () => { readonly firstDay: number }; +}; + +/** + * First weekday of a locale as a `Date#getDay` index (0 is Sunday), or + * `undefined` when the runtime has no week data, so callers keep their own + * default. Without a locale it reads the runtime's. + */ +export function resolveWeekStartsOn(locale: string | undefined): WeekdayIndex | undefined { + try { + const resolved: LocaleWithWeekInfo = new Intl.Locale( + locale ?? Intl.DateTimeFormat().resolvedOptions().locale, + ); + // Week info counts Monday as 1 and Sunday as 7. + const firstDay = resolved.getWeekInfo?.().firstDay ?? resolved.weekInfo?.firstDay; + return firstDay === undefined ? undefined : WEEKDAY_INDEXES[firstDay % 7]; + } catch { + return undefined; + } +} + +/** Week start for calendars, from the same locale timestamps are shown in. */ +export const weekStartsOn = resolveWeekStartsOn(timestampLocale); + const timestampFormatterCache = new Map(); function getTimestampFormatter( -- 2.54.0