Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions apps/web/src/components/Sidebar.snooze.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function localDate(year: number, month: number, day: number, hour: number, minut
describe("resolveSnoozePresets", () => {
it("offers one hour, three hours, evening, tomorrow, and next week in the morning", () => {
// Wednesday 2026-04-08 10:00 local.
const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale");
const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale", "en-GB");
expect(presets.map((preset) => preset.id)).toEqual([
"hour",
"three-hours",
Expand All @@ -33,7 +33,7 @@ describe("resolveSnoozePresets", () => {
});

it("whenLabel complements the label instead of repeating it", () => {
const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale");
const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10), "locale", "en-GB");
for (const preset of presets) {
// Day words live in the label column; the time column is time-only
// (plus a weekday for next week, which names a different day).
Expand All @@ -47,16 +47,18 @@ describe("resolveSnoozePresets", () => {

it("drops the evening preset once evening is near or past", () => {
expect(
resolveSnoozePresets(localDate(2026, 4, 8, 17, 30), "locale").map((preset) => preset.id),
resolveSnoozePresets(localDate(2026, 4, 8, 17, 30), "locale", "en-GB").map(
(preset) => preset.id,
),
).toEqual(["hour", "three-hours", "tomorrow", "next-week"]);
expect(
resolveSnoozePresets(localDate(2026, 4, 8, 21), "locale").map((preset) => preset.id),
resolveSnoozePresets(localDate(2026, 4, 8, 21), "locale", "en-GB").map((preset) => preset.id),
).toEqual(["hour", "three-hours", "tomorrow", "next-week"]);
});

it("puts next week a full week out when today is Monday", () => {
// Monday 2026-04-06.
const presets = resolveSnoozePresets(localDate(2026, 4, 6, 10), "locale");
const presets = resolveSnoozePresets(localDate(2026, 4, 6, 10), "locale", "en-GB");
const nextWeek = new Date(presets.find((preset) => preset.id === "next-week")!.snoozedUntil);
expect(nextWeek.getDay()).toBe(1);
expect(nextWeek.getDate()).toBe(13);
Expand Down Expand Up @@ -93,4 +95,16 @@ describe("snoozeWakeDescription", () => {
"18:00",
);
});

it("uses the client locale for weekday and calendar-date labels", () => {
const weekdayWake = localDate(2026, 4, 13, 9);
const dateWake = localDate(2026, 4, 20, 9);

expect(snoozeWakeDescription(weekdayWake.toISOString(), now, "locale", "de-DE")).toContain(
weekdayWake.toLocaleDateString("de-DE", { weekday: "short" }),
);
expect(snoozeWakeDescription(dateWake.toISOString(), now, "locale", "de-DE")).toContain(
dateWake.toLocaleDateString("de-DE", { month: "short", day: "numeric" }),
);
});
});
12 changes: 7 additions & 5 deletions apps/web/src/components/Sidebar.snooze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type SnoozePreset,
} from "@t3tools/client-runtime/state/thread-settled";

import { formatShortTimestamp, parseTimestampDate } from "../timestampFormat";
import { formatShortTimestamp, getTimestampLocale, parseTimestampDate } from "../timestampFormat";

export { snoozeWakeLabel, type SnoozePreset };

Expand All @@ -18,16 +18,17 @@ function timeOfDayLabel(date: Date, timestampFormat: TimestampFormat): string {
export function resolveSnoozePresets(
now: Date,
timestampFormat: TimestampFormat,
locale: string | undefined = getTimestampLocale(),
): ReadonlyArray<SnoozePreset> {
return resolveSharedSnoozePresets(now).map((preset) => {
return resolveSharedSnoozePresets(now, locale).map((preset) => {
const wake = parseTimestampDate(preset.snoozedUntil);
if (wake === null) return preset;
const time = timeOfDayLabel(wake, timestampFormat);
return {
...preset,
whenLabel:
preset.id === "next-week"
? `${wake.toLocaleDateString(undefined, { weekday: "short" })} ${time}`
? `${wake.toLocaleDateString(locale, { weekday: "short" })} ${time}`
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
: time,
};
});
Expand All @@ -41,6 +42,7 @@ export function snoozeWakeDescription(
snoozedUntil: string,
now: Date,
timestampFormat: TimestampFormat,
locale: string | undefined = getTimestampLocale(),
): string {
const wake = parseTimestampDate(snoozedUntil);
if (wake === null) return "";
Expand All @@ -50,8 +52,8 @@ export function snoozeWakeDescription(
const dayDelta = Math.floor((wake.getTime() - startOfToday.getTime()) / DAY_MS);
if (dayDelta === 0) return time;
if (dayDelta === 1) return `tomorrow ${time}`;
const weekday = wake.toLocaleDateString(undefined, { weekday: "short" });
const weekday = wake.toLocaleDateString(locale, { weekday: "short" });
if (dayDelta < 7) return `${weekday} ${time}`;
const date = wake.toLocaleDateString(undefined, { month: "short", day: "numeric" });
const date = wake.toLocaleDateString(locale, { month: "short", day: "numeric" });
return `${date}, ${time}`;
}
4 changes: 4 additions & 0 deletions apps/web/src/timestampFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function readHostSystemLocale(): string | null {

const timestampLocale = resolveTimestampLocale(readHostSystemLocale());

export function getTimestampLocale(): string | undefined {
return timestampLocale;
}

const timestampFormatterCache = new Map<string, Intl.DateTimeFormat>();

function getTimestampFormatter(
Expand Down
31 changes: 25 additions & 6 deletions packages/client-runtime/src/state/threadSettled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,32 @@ function addSnoozeDays(base: Date, days: number): Date {
return next;
}

type LocaleWeekInfo = { readonly firstDay: number };
type LocaleWithWeekInfo = Intl.Locale & {
readonly weekInfo?: LocaleWeekInfo;
getWeekInfo?: () => LocaleWeekInfo;
};

function firstDayOfWeek(locale: string | undefined): number {
try {
const resolvedLocale = new Intl.Locale(
locale ?? Intl.DateTimeFormat().resolvedOptions().locale,
) as LocaleWithWeekInfo;
const firstDay = resolvedLocale.getWeekInfo?.().firstDay ?? resolvedLocale.weekInfo?.firstDay;
return firstDay === undefined ? 1 : firstDay % 7;
} catch {
return 1;
}
}

/**
* Shared "snooze until" choices for every client. "This evening" only
* appears while it is meaningfully before evening; after that the calendar
* choices start at "Tomorrow". Calendar presets that land on the same
* instant collapse: on Sundays "Tomorrow" and "Next week" are both Monday
* morning, so only "Tomorrow" is offered.
* choices start at "Tomorrow". "Next week" follows the locale's first
* weekday. Calendar presets that land on the same instant collapse, so only
* "Tomorrow" is offered when tomorrow starts the next locale week.
*/
export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {
export function resolveSnoozePresets(now: Date, locale?: string): ReadonlyArray<SnoozePreset> {
const inAnHour = new Date(now.getTime() + HOUR_MS);
const inThreeHours = new Date(now.getTime() + 3 * HOUR_MS);
const presets: SnoozePreset[] = [
Expand Down Expand Up @@ -245,8 +263,9 @@ export function resolveSnoozePresets(now: Date): ReadonlyArray<SnoozePreset> {
snoozedUntil: tomorrow.toISOString(),
});

const daysUntilMonday = (1 - now.getDay() + 7) % 7 || 7;
const nextWeek = snoozeAtHour(addSnoozeDays(now, daysUntilMonday), MORNING_HOUR);
const weekStart = firstDayOfWeek(locale);
const daysUntilNextWeek = (weekStart - now.getDay() + 7) % 7 || 7;
const nextWeek = snoozeAtHour(addSnoozeDays(now, daysUntilNextWeek), MORNING_HOUR);
if (nextWeek.getTime() !== tomorrow.getTime()) {
presets.push({
id: "next-week",
Expand Down
23 changes: 14 additions & 9 deletions packages/client-runtime/src/state/threadSnoozed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,18 +349,23 @@ describe("resolveSnoozePresets", () => {
]);
});

it("puts next week on the following Monday", () => {
const nextWeek = new Date(
resolveSnoozePresets(localDate(2026, 4, 6, 10)).find((preset) => preset.id === "next-week")!
.snoozedUntil,
it("puts next week on the locale's first weekday", () => {
const now = localDate(2026, 4, 6, 10);
const sundayStart = new Date(
resolveSnoozePresets(now, "en-US").find((preset) => preset.id === "next-week")!.snoozedUntil,
);
expect(nextWeek.getDay()).toBe(1);
expect(nextWeek.getDate()).toBe(13);
const mondayStart = new Date(
resolveSnoozePresets(now, "en-GB").find((preset) => preset.id === "next-week")!.snoozedUntil,
);

expect(sundayStart.getDay()).toBe(0);
expect(sundayStart.getDate()).toBe(12);
expect(mondayStart.getDay()).toBe(1);
expect(mondayStart.getDate()).toBe(13);
});

it("drops next week on Sundays, when it lands on the same Monday as tomorrow", () => {
// Sunday 2026-08-30 07:01: "Tomorrow" and "Next week" are both Monday 9:00.
const presets = resolveSnoozePresets(localDate(2026, 8, 30, 7, 1));
it("drops next week when it lands on the same day as tomorrow", () => {
const presets = resolveSnoozePresets(localDate(2026, 8, 30, 7, 1), "en-GB");
expect(presets.map((preset) => preset.id)).toEqual([
"hour",
"three-hours",
Expand Down
Loading