feat: add user-selectable regional date format

This commit is contained in:
honzup
2026-07-04 14:53:37 +02:00
committed by Linus Rath
parent a099ab442a
commit 14c2807f0a
23 changed files with 241 additions and 16 deletions
+17 -4
View File
@@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl';
import { LanguageSwitcher } from '@/components/ui/language-switcher'; import { LanguageSwitcher } from '@/components/ui/language-switcher';
import { useLocaleStore } from '@/stores/locale-store'; import { useLocaleStore } from '@/stores/locale-store';
import { useSettingsStore } from '@/stores/settings-store'; import { useSettingsStore } from '@/stores/settings-store';
import type { DateFormat, TimeFormat, FirstDayOfWeek } from '@/stores/settings-store'; import type { DateFormat, DateLocale, TimeFormat, FirstDayOfWeek } from '@/stores/settings-store';
import { formatDate } from '@/lib/utils'; import { formatDate } from '@/lib/utils';
import { SettingsSection, SettingItem, Select, RadioGroup } from './settings-section'; import { SettingsSection, SettingItem, Select, RadioGroup } from './settings-section';
@@ -13,7 +13,7 @@ export function LanguageSettings() {
const t = useTranslations('settings.language_region'); const t = useTranslations('settings.language_region');
const tDays = useTranslations('calendar.days'); const tDays = useTranslations('calendar.days');
const { dateFormat, timeFormat, firstDayOfWeek, updateSetting } = useSettingsStore(); const { dateFormat, dateLocale, timeFormat, firstDayOfWeek, updateSetting } = useSettingsStore();
// Subscribe to locale changes so the preview re-renders on language switch // Subscribe to locale changes so the preview re-renders on language switch
// (formatDate reads it via getState() and would otherwise stay stale). // (formatDate reads it via getState() and would otherwise stay stale).
@@ -23,7 +23,7 @@ export function LanguageSettings() {
// Build sample timestamps for each bucket so users see what their pick // Build sample timestamps for each bucket so users see what their pick
// will look like in practice. Use offsets relative to "now" so the // will look like in practice. Use offsets relative to "now" so the
// bucketing is stable even though the wall-clock keeps moving. // bucketing is stable even though the wall-clock keeps moving.
void locale; void dateFormat; void timeFormat; void locale; void dateFormat; void dateLocale; void timeFormat;
const now = new Date(); const now = new Date();
const today = new Date(now); const today = new Date(now);
today.setHours(15, 31, 0, 0); today.setHours(15, 31, 0, 0);
@@ -38,7 +38,7 @@ export function LanguageSettings() {
thisWeek: formatDate(thisWeek), thisWeek: formatDate(thisWeek),
older: formatDate(older), older: formatDate(older),
}; };
}, [locale, dateFormat, timeFormat]); }, [locale, dateFormat, dateLocale, timeFormat]);
return ( return (
<SettingsSection title={t('title')} description={t('description')}> <SettingsSection title={t('title')} description={t('description')}>
@@ -74,6 +74,19 @@ export function LanguageSettings() {
</div> </div>
</SettingItem> </SettingItem>
<SettingItem label={t('date_locale.label')} description={t('date_locale.description')}>
<Select
value={dateLocale}
onChange={(value) => updateSetting('dateLocale', value as DateLocale)}
options={[
{ value: 'auto', label: t('date_locale.auto') },
{ value: 'iso', label: t('date_locale.iso') },
{ value: 'en-GB', label: t('date_locale.dmy') },
{ value: 'en-US', label: t('date_locale.mdy') },
]}
/>
</SettingItem>
<SettingItem label={t('time_format.label')} description={t('time_format.description')}> <SettingItem label={t('time_format.label')} description={t('time_format.description')}>
<RadioGroup <RadioGroup
value={timeFormat} value={timeFormat}
+53 -12
View File
@@ -5,6 +5,7 @@ import type { UnifiedMailboxRole } from "./jmap/types";
import { debug } from "./debug"; import { debug } from "./debug";
import { useLocaleStore } from "@/stores/locale-store"; import { useLocaleStore } from "@/stores/locale-store";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
import type { DateLocale } from "@/stores/settings-store";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -42,6 +43,32 @@ export function generateUUID(): string {
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
} }
/**
* Resolve the Intl locale used to render NUMERIC date parts, honouring the
* user's regional `dateLocale` override while leaving weekday/month names on
* the UI language. `auto` returns `fallback` unchanged (prior behaviour); the
* explicit regions force a fixed numeric ordering (#456):
* - `iso` → `en-CA` (YYYY-MM-DD)
* - `en-GB` → `en-GB` (DD/MM/YYYY)
* - `en-US` → `en-US` (MM/DD/YYYY)
*/
function resolveDateLocale<T extends string | undefined>(
dateLocale: DateLocale,
fallback: T,
): string | T {
switch (dateLocale) {
case "iso":
return "en-CA";
case "en-GB":
return "en-GB";
case "en-US":
return "en-US";
case "auto":
default:
return fallback;
}
}
/** /**
* Formats a received-at date for the email list. The output style is * Formats a received-at date for the email list. The output style is
* controlled by the `dateFormat` user setting: * controlled by the `dateFormat` user setting:
@@ -53,8 +80,13 @@ export function generateUUID(): string {
* - `relative` — legacy en-US relative format ("1h ago", "2d ago"). * - `relative` — legacy en-US relative format ("1h ago", "2d ago").
* - `full` — always the full locale date+time. * - `full` — always the full locale date+time.
* *
* Both the locale (from the language picker) and 12h/24h preference are * The numeric date ordering is additionally governed by the `dateLocale`
* read via `getState()` so this stays SSR-safe. * region setting (`auto` = follow the UI language, unchanged; or a fixed
* ISO / DD-MM / MM-DD ordering). Weekday and month names always follow the
* UI language.
*
* The locale (from the language picker), the region override and the 12h/24h
* preference are all read via `getState()` so this stays SSR-safe.
*/ */
export function formatDate(date: Date | string): string { export function formatDate(date: Date | string): string {
const d = typeof date === "string" ? new Date(date) : date; const d = typeof date === "string" ? new Date(date) : date;
@@ -62,10 +94,14 @@ export function formatDate(date: Date | string): string {
const localeRaw = useLocaleStore.getState().locale; const localeRaw = useLocaleStore.getState().locale;
const locale = localeRaw && localeRaw.length > 0 ? localeRaw : "en"; const locale = localeRaw && localeRaw.length > 0 ? localeRaw : "en";
// `en` alone resolves to en-US in Intl; everything else uses the language // Names (weekday, month) follow the UI language: `en` alone resolves to
// subtag as-is and lets the runtime pick a sensible default region. // en-US in Intl; everything else uses the language subtag as-is and lets
const intlLocale = locale === "en" ? "en-US" : locale; // the runtime pick a sensible default region.
const { dateFormat, timeFormat } = useSettingsStore.getState(); const uiLocale = locale === "en" ? "en-US" : locale;
const { dateFormat, dateLocale, timeFormat } = useSettingsStore.getState();
// Numeric dates additionally honour the regional `dateLocale` override
// (defaults to `auto` = the UI language, preserving prior behaviour). (#456)
const numericLocale = resolveDateLocale(dateLocale, uiLocale);
const hour12 = timeFormat === "12h"; const hour12 = timeFormat === "12h";
if (dateFormat === "relative") { if (dateFormat === "relative") {
@@ -77,7 +113,7 @@ export function formatDate(date: Date | string): string {
if (minutes < 60) return `${minutes}m ago`; if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`; if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`; if (days < 7) return `${days}d ago`;
return d.toLocaleDateString(intlLocale, { return d.toLocaleDateString(uiLocale, {
month: "short", month: "short",
day: "numeric", day: "numeric",
year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined, year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
@@ -85,7 +121,7 @@ export function formatDate(date: Date | string): string {
} }
if (dateFormat === "full") { if (dateFormat === "full") {
return d.toLocaleString(intlLocale, { return d.toLocaleString(numericLocale, {
year: "numeric", year: "numeric",
month: "2-digit", month: "2-digit",
day: "2-digit", day: "2-digit",
@@ -96,7 +132,7 @@ export function formatDate(date: Date | string): string {
} }
// 'smart' (default) // 'smart' (default)
const timeStr = d.toLocaleTimeString(intlLocale, { const timeStr = d.toLocaleTimeString(uiLocale, {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
hour12, hour12,
@@ -113,12 +149,12 @@ export function formatDate(date: Date | string): string {
// German Intl outputs "Fr." with a trailing dot for `weekday: 'short'`; // German Intl outputs "Fr." with a trailing dot for `weekday: 'short'`;
// strip it so the result reads cleanly next to the time. // strip it so the result reads cleanly next to the time.
const weekday = d const weekday = d
.toLocaleDateString(intlLocale, { weekday: "short" }) .toLocaleDateString(uiLocale, { weekday: "short" })
.replace(/\.$/, ""); .replace(/\.$/, "");
return `${weekday} ${timeStr}`; return `${weekday} ${timeStr}`;
} }
return d.toLocaleDateString(intlLocale, { return d.toLocaleDateString(numericLocale, {
year: "numeric", year: "numeric",
month: "2-digit", month: "2-digit",
day: "2-digit", day: "2-digit",
@@ -144,6 +180,11 @@ export function formatDateTime(
const d = typeof date === 'string' ? new Date(date) : date; const d = typeof date === 'string' ? new Date(date) : date;
if (isNaN(d.getTime())) return typeof date === 'string' ? date : ''; if (isNaN(d.getTime())) return typeof date === 'string' ? date : '';
// Honour the regional `dateLocale` override; `auto` keeps the previous
// `undefined` (runtime default) locale so existing behaviour is unchanged. (#456)
const { dateLocale } = useSettingsStore.getState();
const effectiveLocale = resolveDateLocale(dateLocale, undefined);
const localeOptions: Intl.DateTimeFormatOptions = {}; const localeOptions: Intl.DateTimeFormatOptions = {};
if (options?.weekday) localeOptions.weekday = options.weekday; if (options?.weekday) localeOptions.weekday = options.weekday;
if (options?.year) localeOptions.year = options.year; if (options?.year) localeOptions.year = options.year;
@@ -158,7 +199,7 @@ export function formatDateTime(
if (options?.timeZoneName) localeOptions.timeZoneName = options.timeZoneName; if (options?.timeZoneName) localeOptions.timeZoneName = options.timeZoneName;
} }
return d.toLocaleString(undefined, localeOptions); return d.toLocaleString(effectiveLocale, localeOptions);
} }
// Marketing emails pad the preheader with whitespace, format chars (soft // Marketing emails pad the preheader with whitespace, format chars (soft
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Tento týden:", "preview_this_week": "Tento týden:",
"preview_older": "Starší:" "preview_older": "Starší:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Formát času", "label": "Formát času",
"description": "Vyberte 12hodinový nebo 24hodinový formát času", "description": "Vyberte 12hodinový nebo 24hodinový formát času",
+8
View File
@@ -1050,6 +1050,14 @@
"preview_this_week": "Denne uge:", "preview_this_week": "Denne uge:",
"preview_older": "Ældre:" "preview_older": "Ældre:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Tidsformat", "label": "Tidsformat",
"description": "Vælg mellem 12-timers eller 24-timers ur", "description": "Vælg mellem 12-timers eller 24-timers ur",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Diese Woche:", "preview_this_week": "Diese Woche:",
"preview_older": "Älter:" "preview_older": "Älter:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Zeitformat", "label": "Zeitformat",
"description": "Wählen Sie zwischen 12-Stunden- oder 24-Stunden-Anzeige", "description": "Wählen Sie zwischen 12-Stunden- oder 24-Stunden-Anzeige",
+8
View File
@@ -1050,6 +1050,14 @@
"preview_this_week": "This week:", "preview_this_week": "This week:",
"preview_older": "Older:" "preview_older": "Older:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Time Format", "label": "Time Format",
"description": "Choose between 12-hour or 24-hour clock", "description": "Choose between 12-hour or 24-hour clock",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Esta semana:", "preview_this_week": "Esta semana:",
"preview_older": "Más antiguo:" "preview_older": "Más antiguo:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Formato de Hora", "label": "Formato de Hora",
"description": "Elija entre reloj de 12 o 24 horas", "description": "Elija entre reloj de 12 o 24 horas",
+8
View File
@@ -1050,6 +1050,14 @@
"preview_this_week": "این هفته:", "preview_this_week": "این هفته:",
"preview_older": "قدیمی‌تر:" "preview_older": "قدیمی‌تر:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "فرمت زمان", "label": "فرمت زمان",
"description": "۱۲ یا ۲۴ ساعته", "description": "۱۲ یا ۲۴ ساعته",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Cette semaine :", "preview_this_week": "Cette semaine :",
"preview_older": "Plus ancien :" "preview_older": "Plus ancien :"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Format d'heure", "label": "Format d'heure",
"description": "Choisissez entre 12 heures ou 24 heures", "description": "Choisissez entre 12 heures ou 24 heures",
+8
View File
@@ -1050,6 +1050,14 @@
"preview_this_week": "Ezen a héten:", "preview_this_week": "Ezen a héten:",
"preview_older": "Régebbi:" "preview_older": "Régebbi:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Idő formátum", "label": "Idő formátum",
"description": "Válassz a 12 vagy 24 órás időformátum között", "description": "Válassz a 12 vagy 24 órás időformátum között",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Questa settimana:", "preview_this_week": "Questa settimana:",
"preview_older": "Più vecchio:" "preview_older": "Più vecchio:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Formato ora", "label": "Formato ora",
"description": "Scegli tra formato 12 o 24 ore", "description": "Scegli tra formato 12 o 24 ore",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "今週:", "preview_this_week": "今週:",
"preview_older": "それ以前:" "preview_older": "それ以前:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "時刻形式", "label": "時刻形式",
"description": "12時間制または24時間制を選択", "description": "12時間制または24時間制を選択",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "이번 주:", "preview_this_week": "이번 주:",
"preview_older": "이전:" "preview_older": "이전:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "시간 형식", "label": "시간 형식",
"description": "12시간제 또는 24시간제를 선택해 주세요", "description": "12시간제 또는 24시간제를 선택해 주세요",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Šajā nedēļā:", "preview_this_week": "Šajā nedēļā:",
"preview_older": "Vecāks:" "preview_older": "Vecāks:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Laika formāts", "label": "Laika formāts",
"description": "Izvēlieties starp 12 vai 24 stundu formātu", "description": "Izvēlieties starp 12 vai 24 stundu formātu",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Deze week:", "preview_this_week": "Deze week:",
"preview_older": "Ouder:" "preview_older": "Ouder:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Tijdnotatie", "label": "Tijdnotatie",
"description": "Kies tussen 12-uurs of 24-uurs klok", "description": "Kies tussen 12-uurs of 24-uurs klok",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "W tym tygodniu:", "preview_this_week": "W tym tygodniu:",
"preview_older": "Starsze:" "preview_older": "Starsze:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Format czasu", "label": "Format czasu",
"description": "Wybierz zegar 12-godzinny lub 24-godzinny", "description": "Wybierz zegar 12-godzinny lub 24-godzinny",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Esta semana:", "preview_this_week": "Esta semana:",
"preview_older": "Mais antigo:" "preview_older": "Mais antigo:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Formato de Hora", "label": "Formato de Hora",
"description": "Escolha entre relógio de 12 ou 24 horas", "description": "Escolha entre relógio de 12 ou 24 horas",
+8
View File
@@ -1050,6 +1050,14 @@
"preview_this_week": "În această săptămână:", "preview_this_week": "În această săptămână:",
"preview_older": "Versiune anterioară:" "preview_older": "Versiune anterioară:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Formatul orei", "label": "Formatul orei",
"description": "Alegeți între formatul de 12 ore sau cel de 24 de ore", "description": "Alegeți între formatul de 12 ore sau cel de 24 de ore",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "На этой неделе:", "preview_this_week": "На этой неделе:",
"preview_older": "Старее:" "preview_older": "Старее:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Формат времени", "label": "Формат времени",
"description": "Выберите формат 12- или 24-часовой нотации", "description": "Выберите формат 12- или 24-часовой нотации",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Bu hafta:", "preview_this_week": "Bu hafta:",
"preview_older": "Daha eski:" "preview_older": "Daha eski:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Saat Biçimi", "label": "Saat Biçimi",
"description": "12 saatlik veya 24 saatlik saat arasında seçin", "description": "12 saatlik veya 24 saatlik saat arasında seçin",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "Цього тижня:", "preview_this_week": "Цього тижня:",
"preview_older": "Старіше:" "preview_older": "Старіше:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "Формат часу", "label": "Формат часу",
"description": "Виберіть 12-годинний або 24-годинний формат годинника", "description": "Виберіть 12-годинний або 24-годинний формат годинника",
+8
View File
@@ -1047,6 +1047,14 @@
"preview_this_week": "本周:", "preview_this_week": "本周:",
"preview_older": "更早:" "preview_older": "更早:"
}, },
"date_locale": {
"label": "Date format region",
"description": "How numeric dates are ordered (day, month, year)",
"auto": "Automatic (match language)",
"iso": "ISO 8601 (YYYY-MM-DD)",
"dmy": "Day/Month/Year",
"mdy": "Month/Day/Year"
},
"time_format": { "time_format": {
"label": "时间格式", "label": "时间格式",
"description": "选择 12 小时制或 24 小时制", "description": "选择 12 小时制或 24 小时制",
+11
View File
@@ -39,6 +39,14 @@ export type SignaturePosition = 'above_quote' | 'below_quote';
/** How to handle an incoming Disposition-Notification-To (read-receipt) request. */ /** How to handle an incoming Disposition-Notification-To (read-receipt) request. */
export type ReadReceiptResponse = 'ask' | 'always' | 'never'; export type ReadReceiptResponse = 'ask' | 'always' | 'never';
export type DateFormat = 'smart' | 'relative' | 'full'; export type DateFormat = 'smart' | 'relative' | 'full';
/**
* Regional ordering of numeric dates, independent of the `DateFormat` style.
* - `auto` follow the UI language (today's behaviour).
* - `iso` ISO 8601, `YYYY-MM-DD`.
* - `en-GB` Day/Month/Year (`DD/MM/YYYY`).
* - `en-US` Month/Day/Year (`MM/DD/YYYY`).
*/
export type DateLocale = 'auto' | 'iso' | 'en-GB' | 'en-US';
export type TimeFormat = '12h' | '24h'; export type TimeFormat = '12h' | '24h';
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
export type ExternalContentPolicy = 'ask' | 'block' | 'allow'; export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
@@ -135,6 +143,7 @@ interface SettingsState {
// Language & Region // Language & Region
dateFormat: DateFormat; dateFormat: DateFormat;
dateLocale: DateLocale;
timeFormat: TimeFormat; timeFormat: TimeFormat;
firstDayOfWeek: FirstDayOfWeek; firstDayOfWeek: FirstDayOfWeek;
@@ -346,6 +355,7 @@ const DEFAULT_SETTINGS = {
// Language & Region // Language & Region
dateFormat: 'smart' as DateFormat, dateFormat: 'smart' as DateFormat,
dateLocale: 'auto' as DateLocale,
timeFormat: '24h' as TimeFormat, timeFormat: '24h' as TimeFormat,
firstDayOfWeek: 1 as FirstDayOfWeek, // Monday firstDayOfWeek: 1 as FirstDayOfWeek, // Monday
@@ -561,6 +571,7 @@ export const useSettingsStore = create<SettingsState>()(
density: state.density, density: state.density,
animationsEnabled: state.animationsEnabled, animationsEnabled: state.animationsEnabled,
dateFormat: state.dateFormat, dateFormat: state.dateFormat,
dateLocale: state.dateLocale,
timeFormat: state.timeFormat, timeFormat: state.timeFormat,
firstDayOfWeek: state.firstDayOfWeek, firstDayOfWeek: state.firstDayOfWeek,
markAsReadDelay: state.markAsReadDelay, markAsReadDelay: state.markAsReadDelay,