From 06c2f637a3dbee405f26d794ac1771cbd12e5905 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Mon, 27 Apr 2026 18:58:27 +0300
Subject: [PATCH] Add "Date format" preference
---
app/db/tables.ts | 9 ++++++
.../settings/actions/settings.server.ts | 6 ++++
app/features/settings/routes/settings.tsx | 13 ++++++++
app/features/settings/settings-schemas.ts | 14 ++++++++
app/hooks/useTimeFormat.ts | 32 +++++++++++++++++--
locales/da/forms.json | 5 +++
locales/de/forms.json | 5 +++
locales/en/forms.json | 5 +++
locales/es-ES/forms.json | 5 +++
locales/es-US/forms.json | 5 +++
locales/fr-CA/forms.json | 5 +++
locales/fr-EU/forms.json | 5 +++
locales/he/forms.json | 5 +++
locales/it/forms.json | 5 +++
locales/ja/forms.json | 5 +++
locales/ko/forms.json | 5 +++
locales/nl/forms.json | 5 +++
locales/pl/forms.json | 5 +++
locales/pt-BR/forms.json | 5 +++
locales/ru/forms.json | 5 +++
locales/zh/forms.json | 5 +++
21 files changed, 152 insertions(+), 2 deletions(-)
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": "",