Add "Date format" preference
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2026-04-27 18:58:27 +03:00
parent ec981d586e
commit 06c2f637a3
21 changed files with 152 additions and 2 deletions

View File

@@ -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. */

View File

@@ -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);
}

View File

@@ -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 }) => <FormField name="newValue" />}
</SendouForm>
) : null}
{user ? (
<SendouForm
schema={dateFormatSchema}
defaultValues={{
newValue: user.preferences.dateFormat ?? "auto",
}}
autoSubmit
revalidateRoot
>
{({ FormField }) => <FormField name="newValue" />}
</SendouForm>
) : null}
{user ? (
<>
<Divider className={styles.divider} smallText>

View File

@@ -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,
]);

View File

@@ -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<NonNullable<UserPreferences["dateFormat"]>, "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";
}

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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": "",