From e55ffccd218e4f4521c71a4a1789307688a596c5 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 30 Mar 2026 18:44:26 +0200 Subject: [PATCH] feat: infer default time format from navigator.language --- app/client/lib/__tests__/datetime.test.ts | 9 +++++ app/client/lib/datetime.ts | 39 +++++++++++++++++++ app/client/modules/auth/routes/onboarding.tsx | 8 ++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/app/client/lib/__tests__/datetime.test.ts b/app/client/lib/__tests__/datetime.test.ts index 5a573ea5..464ef65e 100644 --- a/app/client/lib/__tests__/datetime.test.ts +++ b/app/client/lib/__tests__/datetime.test.ts @@ -8,6 +8,7 @@ import { formatShortDateTime, formatTime, formatTimeAgo, + inferDateTimePreferences, } from "../datetime"; afterEach(() => { @@ -89,6 +90,14 @@ describe("datetime formatters", () => { expect(formatTime(sampleDate, { locale: "en-US", timeZone: "UTC", timeFormat })).toBe(expected); }); + test.each([ + ["en-US", { dateFormat: "MM/DD/YYYY", timeFormat: "12h" }], + ["en-GB", { dateFormat: "DD/MM/YYYY", timeFormat: "24h" }], + ["ja-JP", { dateFormat: "YYYY/MM/DD", timeFormat: "24h" }], + ] as const)("infers date and time preferences for %s", (locale, expected) => { + expect(inferDateTimePreferences(locale)).toEqual(expected); + }); + test("formats combined values with custom date and time preferences", () => { expect( formatDateTime(sampleDate, { diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts index 592cc784..0fb340df 100644 --- a/app/client/lib/datetime.ts +++ b/app/client/lib/datetime.ts @@ -12,6 +12,8 @@ export const TIME_FORMATS = ["12h", "24h"] as const; export type TimeFormatPreference = (typeof TIME_FORMATS)[number]; export const DEFAULT_TIME_FORMAT: TimeFormatPreference = "12h"; +const BROWSER_PREFERENCE_SAMPLE_DATE = new Date(Date.UTC(2006, 0, 2, 15, 4, 5)); + const DATE_PART_ORDERS = { "MM/DD/YYYY": ["month", "day", "year"], "DD/MM/YYYY": ["day", "month", "year"], @@ -109,6 +111,43 @@ function formatConfiguredTime(date: Date, options: DateFormatOptions) { }).format(date); } +export function inferDateTimePreferences(locale?: string) { + const dateOrder = getDateTimeFormat(locale, undefined, { + month: "numeric", + day: "numeric", + year: "numeric", + }) + .formatToParts(BROWSER_PREFERENCE_SAMPLE_DATE) + .flatMap((part) => { + if (part.type === "year" || part.type === "month" || part.type === "day") { + return [part.type]; + } + + return []; + }) + .join("/"); + + let dateFormat = DEFAULT_DATE_FORMAT; + + if (dateOrder === "day/month/year") { + dateFormat = "DD/MM/YYYY"; + } else if (dateOrder === "year/month/day") { + dateFormat = "YYYY/MM/DD"; + } + + let timeFormat: TimeFormatPreference = "12h"; + const hour12 = getDateTimeFormat(locale, undefined, { hour: "numeric" }).resolvedOptions().hour12; + + if (hour12 === false) { + timeFormat = "24h"; + } + + return { + dateFormat, + timeFormat, + }; +} + // 1/10/2026, 2:30 PM export function formatDateTime(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate( diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx index e0041450..ffe10f1f 100644 --- a/app/client/modules/auth/routes/onboarding.tsx +++ b/app/client/modules/auth/routes/onboarding.tsx @@ -20,7 +20,7 @@ import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; import { z } from "zod"; -import { DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT } from "~/client/lib/datetime"; +import { inferDateTimePreferences } from "~/client/lib/datetime"; export const clientMiddleware = [authMiddleware]; @@ -59,10 +59,12 @@ export function OnboardingPage() { return; } + const { dateFormat, timeFormat } = inferDateTimePreferences(navigator.language); + const { data, error } = await authClient.signUp.email({ username: normalizeUsername(values.username), - dateFormat: DEFAULT_DATE_FORMAT, - timeFormat: DEFAULT_TIME_FORMAT, + dateFormat, + timeFormat, password: values.password, email: values.email.toLowerCase().trim(), name: values.username,