authorgravatar for 56691628+Mnigos@users.noreply.github.comIgor Makowski <56691628+Mnigos@users.noreply.github.com> 2026-09-22 00:34:12+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-21 19:34:12-03:00
loge1cbb705273a261d26b489f35a7ddca6567d9663
tree64ef3d9903abdce8d2cad2a3e5a0a496422ae149
parent80d9c181da41ee3890791c9b65018ce093e7bc9e
signature Signed by PGP key B5690EEEBB952194

fix(web): the custom snooze calendar starts the week where the locale does (#12745)


3 files changed, 57 insertions(+), 0 deletions(-)

apps/web/src/components/CustomSnoozeDialog.tsx+2
......@@ -10,6 +10,7 @@ import {
1010import { Button } from "./ui/button";
1111import { CalendarIcon } from "lucide-react";
1212import { Calendar } from "./ui/calendar";
13import { weekStartsOn } from "../timestampFormat";
1314import { Popover, PopoverTrigger, PopoverPopup } from "./ui/popover";
1415import { Input } from "./ui/input";
1516import { Label } from "./ui/label";
......@@ -149,6 +150,7 @@ function CustomSnoozeDialog() {
149150 required
150151 selected={date}
151152 defaultMonth={date}
153 {...(weekStartsOn === undefined ? {} : { weekStartsOn })}
152154 disabled={{ before: new Date(new Date().setHours(0, 0, 0, 0)) }}
153155 onSelect={(selected) => {
154156 setDate(selected);
apps/web/src/timestampFormat.test.ts+26
......@@ -52,6 +52,32 @@ describe("formatShortTimestamp", () => {
5252 });
5353});
5454
55describe("resolveWeekStartsOn", () => {
56 it.each([
57 ["en-US", 0],
58 ["en-GB", 1],
59 ["pl-PL", 1],
60 ["ar-EG", 6],
61 ])("starts the %s week on weekday %i", async (locale, weekday) => {
62 const { resolveWeekStartsOn } = await import("./timestampFormat");
63 expect(resolveWeekStartsOn(locale)).toBe(weekday);
64 });
65
66 it("leaves the default to the caller for a malformed locale", async () => {
67 const { resolveWeekStartsOn } = await import("./timestampFormat");
68 expect(resolveWeekStartsOn("not a locale")).toBeUndefined();
69 });
70
71 it("follows the locale the desktop host reports", async () => {
72 vi.stubGlobal("window", { desktopBridge: { getSystemLocale: () => "en-GB" } });
73 vi.resetModules();
74 const { weekStartsOn } = await import("./timestampFormat");
75 expect(weekStartsOn).toBe(1);
76 vi.unstubAllGlobals();
77 vi.resetModules();
78 });
79});
80
5581describe("formatChatTimestampTooltip", () => {
5682 afterEach(() => {
5783 vi.restoreAllMocks();
apps/web/src/timestampFormat.ts+29
......@@ -53,6 +53,35 @@ function readHostSystemLocale(): string | null {
5353
5454const timestampLocale = resolveTimestampLocale(readHostSystemLocale());
5555
56const WEEKDAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const;
57type WeekdayIndex = (typeof WEEKDAY_INDEXES)[number];
58
59type LocaleWithWeekInfo = Intl.Locale & {
60 readonly weekInfo?: { readonly firstDay: number };
61 getWeekInfo?: () => { readonly firstDay: number };
62};
63
64/**
65 * First weekday of a locale as a `Date#getDay` index (0 is Sunday), or
66 * `undefined` when the runtime has no week data, so callers keep their own
67 * default. Without a locale it reads the runtime's.
68 */
69export function resolveWeekStartsOn(locale: string | undefined): WeekdayIndex | undefined {
70 try {
71 const resolved: LocaleWithWeekInfo = new Intl.Locale(
72 locale ?? Intl.DateTimeFormat().resolvedOptions().locale,
73 );
74 // Week info counts Monday as 1 and Sunday as 7.
75 const firstDay = resolved.getWeekInfo?.().firstDay ?? resolved.weekInfo?.firstDay;
76 return firstDay === undefined ? undefined : WEEKDAY_INDEXES[firstDay % 7];
77 } catch {
78 return undefined;
79 }
80}
81
82/** Week start for calendars, from the same locale timestamps are shown in. */
83export const weekStartsOn = resolveWeekStartsOn(timestampLocale);
84
5685const timestampFormatterCache = new Map<string, Intl.DateTimeFormat>();
5786
5887function getTimestampFormatter(