diff --git a/app/db/tables.ts b/app/db/tables.ts index fa125b60d..d3c333b85 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -986,6 +986,15 @@ export interface UserPreferences { * "12h" = 12 hour format (e.g. 2:00 PM) * */ clockFormat?: "24h" | "12h" | "auto"; + /** + * What numeric date format the user prefers? + * + * "auto" = use the format the active language defaults to (default value) + * "MDY" = month/day/year (e.g. 4/27/2026) + * "DMY" = day/month/year (e.g. 27/04/2026) + * "YMD" = ISO year-month-day (e.g. 2026-04-27) + * */ + dateFormat?: "auto" | "MDY" | "DMY" | "YMD"; /** Is the new widget based user page enabled? (Supporter early preview) */ newProfileEnabled?: boolean; /** Is spoiler-free mode enabled? Hides recent tournament results and scores until the user chooses to reveal them. */ diff --git a/app/features/settings/actions/settings.server.ts b/app/features/settings/actions/settings.server.ts index f05cf873d..6c1ed418e 100644 --- a/app/features/settings/actions/settings.server.ts +++ b/app/features/settings/actions/settings.server.ts @@ -59,6 +59,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); break; } + case "UPDATE_DATE_FORMAT": { + await UserRepository.updatePreferences(user.id, { + dateFormat: data.newValue, + }); + break; + } default: { assertUnreachable(data); } diff --git a/app/features/settings/routes/settings.tsx b/app/features/settings/routes/settings.tsx index 1afedd124..ca2d5ea33 100644 --- a/app/features/settings/routes/settings.tsx +++ b/app/features/settings/routes/settings.tsx @@ -30,6 +30,7 @@ import { action } from "../actions/settings.server"; import { loader } from "../loaders/settings.server"; import { clockFormatSchema, + dateFormatSchema, disableBuildAbilitySortingSchema, disallowScrimPickupsFromUntrustedSchema, spoilerFreeModeSchema, @@ -88,6 +89,18 @@ export default function SettingsPage() { {({ FormField }) => } ) : null} + {user ? ( + + {({ FormField }) => } + + ) : null} {user ? ( <> diff --git a/app/features/settings/settings-schemas.ts b/app/features/settings/settings-schemas.ts index 6ccdaba7c..bfeb6c379 100644 --- a/app/features/settings/settings-schemas.ts +++ b/app/features/settings/settings-schemas.ts @@ -19,6 +19,19 @@ export const clockFormatSchema = z.object({ }), }); +export const dateFormatSchema = z.object({ + _action: stringConstant("UPDATE_DATE_FORMAT"), + newValue: select({ + label: "labels.dateFormat", + items: [ + { value: "auto", label: "options.dateFormat.auto" }, + { value: "MDY", label: "options.dateFormat.MDY" }, + { value: "DMY", label: "options.dateFormat.DMY" }, + { value: "YMD", label: "options.dateFormat.YMD" }, + ], + }), +}); + export const disableBuildAbilitySortingSchema = z.object({ _action: stringConstant("UPDATE_DISABLE_BUILD_ABILITY_SORTING"), newValue: toggle({ @@ -58,4 +71,5 @@ export const settingsEditSchema = z.union([ spoilerFreeModeSchema, updateNoScreenSchema, clockFormatSchema, + dateFormatSchema, ]); diff --git a/app/hooks/useTimeFormat.ts b/app/hooks/useTimeFormat.ts index c6a7d2966..f24359bd2 100644 --- a/app/hooks/useTimeFormat.ts +++ b/app/hooks/useTimeFormat.ts @@ -1,4 +1,5 @@ import { useTranslation } from "react-i18next"; +import type { UserPreferences } from "~/db/tables"; import { useUser } from "~/features/auth/core/user"; import type { LanguageCode } from "~/modules/i18n/config"; import { formatDistanceToNow as formatDistanceToNowUtil } from "~/utils/dates"; @@ -11,6 +12,15 @@ const H24_TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour12: false, hourCycle: "h23" as const, }; + +const DATE_FORMAT_LOCALE: Record< + Exclude, "auto">, + string +> = { + MDY: "en-US", + DMY: "en-GB", + YMD: "sv-SE", +}; function getClockFormatOptions( clockFormat: "auto" | "24h" | "12h" | undefined, language: string, @@ -68,11 +78,13 @@ export function useTimeFormat() { const { i18n } = useTranslation(); const user = useUser(); const clockFormat = user?.preferences?.clockFormat; + const dateFormat = user?.preferences?.dateFormat; const clockOptions = getClockFormatOptions(clockFormat, i18n.language); + const dateLocale = getDateLocale(dateFormat, i18n.language); const formatDateTime = (date: Date, options?: Intl.DateTimeFormatOptions) => { const result = date.toLocaleString( - i18n.language, + isNumericMonth(options) ? dateLocale : i18n.language, options?.hour ? { ...options, @@ -104,7 +116,10 @@ export function useTimeFormat() { }; const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions) => { - return date.toLocaleDateString(i18n.language, options); + return date.toLocaleDateString( + isNumericMonth(options) ? dateLocale : i18n.language, + options, + ); }; /** Same as `formatDateTime` but omits minutes when they are zero and AM/PM format is in use */ @@ -165,3 +180,16 @@ export function useTimeFormat() { function stripLeadingZeroFromHour(timeString: string) { return timeString.replace(/\b0(\d:\d{2})/g, "$1"); } + +function getDateLocale( + dateFormat: UserPreferences["dateFormat"] | undefined, + language: string, +) { + if (!dateFormat || dateFormat === "auto") return language; + return DATE_FORMAT_LOCALE[dateFormat]; +} + +function isNumericMonth(options: Intl.DateTimeFormatOptions | undefined) { + if (!options?.month) return false; + return options.month === "numeric" || options.month === "2-digit"; +} diff --git a/locales/da/forms.json b/locales/da/forms.json index d5a94874d..4c44c9ab2 100644 --- a/locales/da/forms.json +++ b/locales/da/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/de/forms.json b/locales/de/forms.json index 643c38e52..6d22de1c0 100644 --- a/locales/de/forms.json +++ b/locales/de/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/en/forms.json b/locales/en/forms.json index d08e0979a..a10a0d002 100644 --- a/locales/en/forms.json +++ b/locales/en/forms.json @@ -5,6 +5,7 @@ "labels.tag": "Tag", "labels.teamBsky": "Team Bluesky", "labels.clockFormat": "Clock format", + "labels.dateFormat": "Date format", "labels.disableBuildAbilitySorting": "Builds: Disable automatic ability sorting", "labels.disallowScrimPickupsFromUntrusted": "Disallow scrim pickups from non-friends", "labels.noScreen": "[Accessibility] Avoid Splattercolor Screen", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "Automatic", "options.clockFormat.24h": "24-hour", "options.clockFormat.12h": "12-hour", + "options.dateFormat.auto": "Automatic", + "options.dateFormat.MDY": "MM/DD/YYYY", + "options.dateFormat.DMY": "DD/MM/YYYY", + "options.dateFormat.YMD": "YYYY-MM-DD", "errors.required": "This field is required", "errors.minLength": "Must be at least {{min}} characters", "errors.maxLength": "Must be at most {{max}} characters", diff --git a/locales/es-ES/forms.json b/locales/es-ES/forms.json index 9358c00dd..221800b60 100644 --- a/locales/es-ES/forms.json +++ b/locales/es-ES/forms.json @@ -5,6 +5,7 @@ "labels.tag": "Etiqueta", "labels.teamBsky": "Bluesky del equipo", "labels.clockFormat": "Formato de hora", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "Builds: Desactivar orden automático de potenciadores", "labels.disallowScrimPickupsFromUntrusted": "No permitir invitaciones de usuarios no verificados", "labels.noScreen": "[Accesibilidad] Evitar Pantintalla", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "Automático", "options.clockFormat.24h": "24 horas", "options.clockFormat.12h": "12 horas", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "Este campo es obligatorio", "errors.minLength": "Debe tener al menos {{min}} caracteres", "errors.maxLength": "Debe tener como máximo {{max}} caracteres", diff --git a/locales/es-US/forms.json b/locales/es-US/forms.json index dcb5df5af..eb061520c 100644 --- a/locales/es-US/forms.json +++ b/locales/es-US/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/fr-CA/forms.json b/locales/fr-CA/forms.json index 12319dc2b..6ac11be50 100644 --- a/locales/fr-CA/forms.json +++ b/locales/fr-CA/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/fr-EU/forms.json b/locales/fr-EU/forms.json index 5a1195ca0..432386794 100644 --- a/locales/fr-EU/forms.json +++ b/locales/fr-EU/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "Team Bluesky", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/he/forms.json b/locales/he/forms.json index 214dee354..f87308a3f 100644 --- a/locales/he/forms.json +++ b/locales/he/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/it/forms.json b/locales/it/forms.json index f6548787c..1c90b3e1e 100644 --- a/locales/it/forms.json +++ b/locales/it/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "Bluesky del team", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/ja/forms.json b/locales/ja/forms.json index db449d1a8..0c7c5e70f 100644 --- a/locales/ja/forms.json +++ b/locales/ja/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "チームの Bluesky", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/ko/forms.json b/locales/ko/forms.json index 991ec10ae..5a0bb3ff8 100644 --- a/locales/ko/forms.json +++ b/locales/ko/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/nl/forms.json b/locales/nl/forms.json index 0ab281a55..21dfdca79 100644 --- a/locales/nl/forms.json +++ b/locales/nl/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/pl/forms.json b/locales/pl/forms.json index a8f73eec2..4d1a3990a 100644 --- a/locales/pl/forms.json +++ b/locales/pl/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/pt-BR/forms.json b/locales/pt-BR/forms.json index f1484ad19..d504d302c 100644 --- a/locales/pt-BR/forms.json +++ b/locales/pt-BR/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/ru/forms.json b/locales/ru/forms.json index 8ca1382db..8aec4df67 100644 --- a/locales/ru/forms.json +++ b/locales/ru/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "Bluesky команды", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "", diff --git a/locales/zh/forms.json b/locales/zh/forms.json index e467ab873..f2d49a5f9 100644 --- a/locales/zh/forms.json +++ b/locales/zh/forms.json @@ -5,6 +5,7 @@ "labels.tag": "", "labels.teamBsky": "", "labels.clockFormat": "", + "labels.dateFormat": "", "labels.disableBuildAbilitySorting": "", "labels.disallowScrimPickupsFromUntrusted": "", "labels.noScreen": "", @@ -18,6 +19,10 @@ "options.clockFormat.auto": "", "options.clockFormat.24h": "", "options.clockFormat.12h": "", + "options.dateFormat.auto": "", + "options.dateFormat.MDY": "", + "options.dateFormat.DMY": "", + "options.dateFormat.YMD": "", "errors.required": "", "errors.minLength": "", "errors.maxLength": "",