feat: locale-aware date format in email list with preset picker #331

This commit is contained in:
Linus Rath
2026-05-25 16:17:32 +02:00
parent 537707d9ed
commit 534894c38d
21 changed files with 342 additions and 163 deletions
+1 -26
View File
@@ -4,17 +4,14 @@ import { useTranslations } from 'next-intl';
import { useCalendarStore, CalendarViewMode } from '@/stores/calendar-store'; import { useCalendarStore, CalendarViewMode } from '@/stores/calendar-store';
import { useSettingsStore } from '@/stores/settings-store'; import { useSettingsStore } from '@/stores/settings-store';
import { usePolicyStore } from '@/stores/policy-store'; import { usePolicyStore } from '@/stores/policy-store';
import { SettingsSection, SettingItem, Select, RadioGroup, ToggleSwitch } from './settings-section'; import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
export function CalendarSettings() { export function CalendarSettings() {
const t = useTranslations('calendar.settings'); const t = useTranslations('calendar.settings');
const tViews = useTranslations('calendar.views'); const tViews = useTranslations('calendar.views');
const tDays = useTranslations('calendar.days');
const { viewMode, setViewMode } = useCalendarStore(); const { viewMode, setViewMode } = useCalendarStore();
const { const {
timeFormat,
firstDayOfWeek,
showTimeInMonthView, showTimeInMonthView,
showWeekNumbers, showWeekNumbers,
enableCalendarTasks, enableCalendarTasks,
@@ -40,28 +37,6 @@ export function CalendarSettings() {
/> />
</SettingItem> </SettingItem>
<SettingItem label={t('week_starts_on')}>
<Select
value={firstDayOfWeek.toString()}
onChange={(value) => updateSetting('firstDayOfWeek', parseInt(value) as 0 | 1)}
options={[
{ value: '1', label: tDays('monday') },
{ value: '0', label: tDays('sunday') },
]}
/>
</SettingItem>
<SettingItem label={t('time_format')}>
<RadioGroup
value={timeFormat}
onChange={(value) => updateSetting('timeFormat', value as '12h' | '24h')}
options={[
{ value: '12h', label: t('time_format_12h') },
{ value: '24h', label: t('time_format_24h') },
]}
/>
</SettingItem>
<SettingItem <SettingItem
label={t('show_time_in_month_view')} label={t('show_time_in_month_view')}
description={t('show_time_in_month_view_desc')} description={t('show_time_in_month_view_desc')}
+86 -3
View File
@@ -1,17 +1,100 @@
"use client"; "use client";
import { useMemo } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { LanguageSwitcher } from '@/components/ui/language-switcher'; import { LanguageSwitcher } from '@/components/ui/language-switcher';
import { SettingsSection, SettingItem } from './settings-section'; import { useLocaleStore } from '@/stores/locale-store';
import { useSettingsStore } from '@/stores/settings-store';
import type { DateFormat, TimeFormat, FirstDayOfWeek } from '@/stores/settings-store';
import { formatDate } from '@/lib/utils';
import { SettingsSection, SettingItem, Select, RadioGroup } from './settings-section';
export function LanguageSettings() { export function LanguageSettings() {
const t = useTranslations('settings.appearance'); const t = useTranslations('settings.language_region');
const tDays = useTranslations('calendar.days');
const { dateFormat, timeFormat, firstDayOfWeek, updateSetting } = useSettingsStore();
// Subscribe to locale changes so the preview re-renders on language switch
// (formatDate reads it via getState() and would otherwise stay stale).
const locale = useLocaleStore((s) => s.locale);
const preview = useMemo(() => {
// Build sample timestamps for each bucket so users see what their pick
// will look like in practice. Use offsets relative to "now" so the
// bucketing is stable even though the wall-clock keeps moving.
void locale; void dateFormat; void timeFormat;
const now = new Date();
const today = new Date(now);
today.setHours(15, 31, 0, 0);
const thisWeek = new Date(now);
thisWeek.setDate(now.getDate() - 2);
thisWeek.setHours(15, 31, 0, 0);
const older = new Date(now);
older.setMonth(now.getMonth() - 2);
older.setHours(15, 31, 0, 0);
return {
today: formatDate(today),
thisWeek: formatDate(thisWeek),
older: formatDate(older),
};
}, [locale, dateFormat, timeFormat]);
return ( return (
<SettingsSection title={t('language.label')} description={t('language.description')}> <SettingsSection title={t('title')} description={t('description')}>
<SettingItem label={t('language.label')} description={t('language.description')}> <SettingItem label={t('language.label')} description={t('language.description')}>
<LanguageSwitcher /> <LanguageSwitcher />
</SettingItem> </SettingItem>
<SettingItem label={t('date_format.label')} description={t('date_format.description')}>
<div className="flex flex-col items-end gap-2">
<Select
value={dateFormat}
onChange={(value) => updateSetting('dateFormat', value as DateFormat)}
options={[
{ value: 'smart', label: t('date_format.smart') },
{ value: 'relative', label: t('date_format.relative') },
{ value: 'full', label: t('date_format.full') },
]}
/>
<div className="text-xs text-muted-foreground text-right space-y-0.5 font-mono">
<div>
<span className="opacity-70">{t('date_format.preview_today')} </span>
<span className="text-foreground/90">{preview.today}</span>
</div>
<div>
<span className="opacity-70">{t('date_format.preview_this_week')} </span>
<span className="text-foreground/90">{preview.thisWeek}</span>
</div>
<div>
<span className="opacity-70">{t('date_format.preview_older')} </span>
<span className="text-foreground/90">{preview.older}</span>
</div>
</div>
</div>
</SettingItem>
<SettingItem label={t('time_format.label')} description={t('time_format.description')}>
<RadioGroup
value={timeFormat}
onChange={(value) => updateSetting('timeFormat', value as TimeFormat)}
options={[
{ value: '12h', label: t('time_format.12h') },
{ value: '24h', label: t('time_format.24h') },
]}
/>
</SettingItem>
<SettingItem label={t('first_day.label')} description={t('first_day.description')}>
<Select
value={firstDayOfWeek.toString()}
onChange={(value) => updateSetting('firstDayOfWeek', parseInt(value) as FirstDayOfWeek)}
options={[
{ value: '1', label: tDays('monday') },
{ value: '0', label: tDays('sunday') },
]}
/>
</SettingItem>
</SettingsSection> </SettingsSection>
); );
} }
+76 -12
View File
@@ -3,6 +3,8 @@ import { twMerge } from "tailwind-merge";
import { Mailbox, UNIFIED_MAILBOX_IDS } from "./jmap/types"; import { Mailbox, UNIFIED_MAILBOX_IDS } from "./jmap/types";
import type { UnifiedMailboxRole } from "./jmap/types"; import type { UnifiedMailboxRole } from "./jmap/types";
import { debug } from "./debug"; import { debug } from "./debug";
import { useLocaleStore } from "@/stores/locale-store";
import { useSettingsStore } from "@/stores/settings-store";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -40,24 +42,86 @@ 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)}`;
} }
/**
* Formats a received-at date for the email list. The output style is
* controlled by the `dateFormat` user setting:
*
* - `smart` (default) — locale-aware, age-bucketed:
* today → time only ("15:31" or "3:31 PM")
* last 7 days → short weekday+time ("Fr 15:31", "Fri 3:31 PM")
* older → full locale date ("28.04.2026", "04/28/2026")
* - `relative` — legacy en-US relative format ("1h ago", "2d ago").
* - `full` — always the full locale date+time.
*
* Both the locale (from the language picker) and 12h/24h preference are
* 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;
const now = new Date(); const now = new Date();
const diff = now.getTime() - d.getTime();
const minutes = Math.floor(diff / 60000); const localeRaw = useLocaleStore.getState().locale;
const hours = Math.floor(diff / 3600000); const locale = localeRaw && localeRaw.length > 0 ? localeRaw : "en";
const days = Math.floor(diff / 86400000); // `en` alone resolves to en-US in Intl; everything else uses the language
// subtag as-is and lets the runtime pick a sensible default region.
const intlLocale = locale === "en" ? "en-US" : locale;
const { dateFormat, timeFormat } = useSettingsStore.getState();
const hour12 = timeFormat === "12h";
if (minutes < 1) return "Just now"; if (dateFormat === "relative") {
if (minutes < 60) return `${minutes}m ago`; const diff = now.getTime() - d.getTime();
if (hours < 24) return `${hours}h ago`; const minutes = Math.floor(diff / 60000);
if (days < 7) return `${days}d ago`; const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "Just now";
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return d.toLocaleDateString(intlLocale, {
month: "short",
day: "numeric",
year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
});
}
return d.toLocaleDateString("en-US", { if (dateFormat === "full") {
month: "short", return d.toLocaleString(intlLocale, {
day: "numeric", year: "numeric",
year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined, month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12,
});
}
// 'smart' (default)
const timeStr = d.toLocaleTimeString(intlLocale, {
hour: "2-digit",
minute: "2-digit",
hour12,
});
const isSameDay =
d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate();
if (isSameDay) return timeStr;
const daysAgo = Math.floor((now.getTime() - d.getTime()) / 86400000);
if (daysAgo < 7) {
// German Intl outputs "Fr." with a trailing dot for `weekday: 'short'`;
// strip it so the result reads cleanly next to the time.
const weekday = d
.toLocaleDateString(intlLocale, { weekday: "short" })
.replace(/\.$/, "");
return `${weekday} ${timeStr}`;
}
return d.toLocaleDateString(intlLocale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}); });
} }
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Pokračovat v úpravách", "keep_editing": "Pokračovat v úpravách",
"tabs": { "tabs": {
"appearance": "Vzhled", "appearance": "Vzhled",
"language": "Jazyk a region", "language": "Jazyk, region a čas",
"email": "Chování e-mailu", "email": "Chování e-mailu",
"composer": "Psaní zpráv", "composer": "Psaní zpráv",
"privacy": "Soukromí a bezpečnost", "privacy": "Soukromí a bezpečnost",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Jazyk a region", "title": "Jazyk, region a čas",
"description": "Nakonfigurujte jazykové a místní předvolby", "description": "Jazyk, formát data, formát času a další regionální předvolby",
"language": { "language": {
"label": "Jazyk", "label": "Jazyk",
"description": "Vyberte preferovaný jazyk", "description": "Vyberte preferovaný jazyk",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Formát data", "label": "Formát data",
"description": "Jak se mají zobrazovat data", "description": "Jak se zobrazují data v seznamu e-mailů",
"regional": "Místní", "smart": "Chytrý (regionál)",
"iso": "ISO 8601", "relative": "Relativní (před 1 h, před 2 d)",
"custom": "Vlastní" "full": "Vždy úplné datum",
"preview_today": "Dnes:",
"preview_this_week": "Tento týden:",
"preview_older": "Starší:"
}, },
"time_format": { "time_format": {
"label": "Formát času", "label": "Formát času",
+10 -7
View File
@@ -755,7 +755,7 @@
"search_no_results": "Ingen match i indstillinger", "search_no_results": "Ingen match i indstillinger",
"tabs": { "tabs": {
"appearance": "Udseende", "appearance": "Udseende",
"language": "Sprog & region", "language": "Sprog, region & tid",
"email": "E-mail-adfærd", "email": "E-mail-adfærd",
"composer": "Komponist", "composer": "Komponist",
"privacy": "Privatliv & sikkerhed", "privacy": "Privatliv & sikkerhed",
@@ -935,8 +935,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Sprog & region", "title": "Sprog, region & tid",
"description": "Konfigurér sprog og regionale præferencer", "description": "Sprog, datoformat, tidsformat og andre regionale indstillinger",
"language": { "language": {
"label": "Sprog", "label": "Sprog",
"description": "Vælg dit foretrukne sprog", "description": "Vælg dit foretrukne sprog",
@@ -945,10 +945,13 @@
}, },
"date_format": { "date_format": {
"label": "Datoformat", "label": "Datoformat",
"description": "Hvordan datoer skal vises", "description": "Sådan vises datoer på e-mail-listen",
"regional": "Regionalt", "smart": "Smart (regionalt)",
"iso": "ISO 8601", "relative": "Relativ (for 1 t siden, for 2 d siden)",
"custom": "Brugerdefineret" "full": "Altid fuld dato",
"preview_today": "I dag:",
"preview_this_week": "Denne uge:",
"preview_older": "Ældre:"
}, },
"time_format": { "time_format": {
"label": "Tidsformat", "label": "Tidsformat",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Weiter bearbeiten", "keep_editing": "Weiter bearbeiten",
"tabs": { "tabs": {
"appearance": "Darstellung", "appearance": "Darstellung",
"language": "Sprache & Region", "language": "Sprache, Region & Zeit",
"email": "E-Mail-Verhalten", "email": "E-Mail-Verhalten",
"composer": "Editor", "composer": "Editor",
"privacy": "Datenschutz & Sicherheit", "privacy": "Datenschutz & Sicherheit",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Sprache & Region", "title": "Sprache, Region & Zeit",
"description": "Konfigurieren Sie Sprach- und Regionaleinstellungen", "description": "Sprache, Datums- und Zeitformat sowie weitere regionale Einstellungen",
"language": { "language": {
"label": "Sprache", "label": "Sprache",
"description": "Wählen Sie Ihre bevorzugte Sprache", "description": "Wählen Sie Ihre bevorzugte Sprache",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Datumsformat", "label": "Datumsformat",
"description": "Wie Daten angezeigt werden sollen", "description": "Wie Daten in der E-Mail-Liste angezeigt werden",
"regional": "Regional", "smart": "Intelligent (gebietsschemaabhängig)",
"iso": "ISO 8601", "relative": "Relativ (vor 1 Std., vor 2 Tagen)",
"custom": "Benutzerdefiniert" "full": "Immer vollständiges Datum",
"preview_today": "Heute:",
"preview_this_week": "Diese Woche:",
"preview_older": "Älter:"
}, },
"time_format": { "time_format": {
"label": "Zeitformat", "label": "Zeitformat",
+10 -7
View File
@@ -755,7 +755,7 @@
"search_no_results": "No matching settings", "search_no_results": "No matching settings",
"tabs": { "tabs": {
"appearance": "Appearance", "appearance": "Appearance",
"language": "Language & Region", "language": "Language, Region & Time",
"email": "Email Behavior", "email": "Email Behavior",
"composer": "Composer", "composer": "Composer",
"privacy": "Privacy & Security", "privacy": "Privacy & Security",
@@ -935,8 +935,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Language & Region", "title": "Language, Region & Time",
"description": "Configure language and regional preferences", "description": "Language, date format, time format, and other regional preferences",
"language": { "language": {
"label": "Language", "label": "Language",
"description": "Choose your preferred language", "description": "Choose your preferred language",
@@ -945,10 +945,13 @@
}, },
"date_format": { "date_format": {
"label": "Date Format", "label": "Date Format",
"description": "How dates should be displayed", "description": "How dates are shown in the email list",
"regional": "Regional", "smart": "Smart (locale-aware)",
"iso": "ISO 8601", "relative": "Relative (1h ago, 2d ago)",
"custom": "Custom" "full": "Always full date",
"preview_today": "Today:",
"preview_this_week": "This week:",
"preview_older": "Older:"
}, },
"time_format": { "time_format": {
"label": "Time Format", "label": "Time Format",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Seguir editando", "keep_editing": "Seguir editando",
"tabs": { "tabs": {
"appearance": "Apariencia", "appearance": "Apariencia",
"language": "Idioma y Región", "language": "Idioma, Región y Hora",
"email": "Comportamiento del Correo", "email": "Comportamiento del Correo",
"composer": "Editor", "composer": "Editor",
"privacy": "Privacidad y Seguridad", "privacy": "Privacidad y Seguridad",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Idioma y Región", "title": "Idioma, Región y Hora",
"description": "Configure las preferencias de idioma y región", "description": "Idioma, formato de fecha, formato de hora y otras preferencias regionales",
"language": { "language": {
"label": "Idioma", "label": "Idioma",
"description": "Elija su idioma preferido", "description": "Elija su idioma preferido",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Formato de Fecha", "label": "Formato de Fecha",
"description": "Cómo se deben mostrar las fechas", "description": "Cómo se muestran las fechas en la lista de correos",
"regional": "Regional", "smart": "Inteligente (según región)",
"iso": "ISO 8601", "relative": "Relativo (hace 1 h, hace 2 d)",
"custom": "Personalizado" "full": "Fecha completa siempre",
"preview_today": "Hoy:",
"preview_this_week": "Esta semana:",
"preview_older": "Más antiguo:"
}, },
"time_format": { "time_format": {
"label": "Formato de Hora", "label": "Formato de Hora",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Continuer l'édition", "keep_editing": "Continuer l'édition",
"tabs": { "tabs": {
"appearance": "Apparence", "appearance": "Apparence",
"language": "Langue et région", "language": "Langue, région et heure",
"email": "Comportement email", "email": "Comportement email",
"composer": "Compositeur", "composer": "Compositeur",
"privacy": "Confidentialité et sécurité", "privacy": "Confidentialité et sécurité",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Langue et région", "title": "Langue, région et heure",
"description": "Configurez vos préférences linguistiques et régionales", "description": "Langue, format de date, format d'heure et autres préférences régionales",
"language": { "language": {
"label": "Langue", "label": "Langue",
"description": "Choisissez votre langue préférée", "description": "Choisissez votre langue préférée",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Format de date", "label": "Format de date",
"description": "Comment les dates doivent être affichées", "description": "Comment les dates apparaissent dans la liste des e-mails",
"regional": "Régional", "smart": "Intelligent (adapté à la région)",
"iso": "ISO 8601", "relative": "Relatif (il y a 1 h, il y a 2 j)",
"custom": "Personnalisé" "full": "Toujours la date complète",
"preview_today": "Aujourd'hui :",
"preview_this_week": "Cette semaine :",
"preview_older": "Plus ancien :"
}, },
"time_format": { "time_format": {
"label": "Format d'heure", "label": "Format d'heure",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Continua a modificare", "keep_editing": "Continua a modificare",
"tabs": { "tabs": {
"appearance": "Aspetto", "appearance": "Aspetto",
"language": "Lingua e regione", "language": "Lingua, regione e ora",
"email": "Comportamento email", "email": "Comportamento email",
"composer": "Editor", "composer": "Editor",
"privacy": "Privacy e sicurezza", "privacy": "Privacy e sicurezza",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Lingua e regione", "title": "Lingua, regione e ora",
"description": "Configura le preferenze di lingua e regionali", "description": "Lingua, formato data, formato ora e altre preferenze regionali",
"language": { "language": {
"label": "Lingua", "label": "Lingua",
"description": "Scegli la tua lingua preferita", "description": "Scegli la tua lingua preferita",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Formato data", "label": "Formato data",
"description": "Come devono essere visualizzate le date", "description": "Come vengono visualizzate le date nell'elenco delle e-mail",
"regional": "Regionale", "smart": "Intelligente (in base alla regione)",
"iso": "ISO 8601", "relative": "Relativo (1 h fa, 2 g fa)",
"custom": "Personalizzato" "full": "Sempre data completa",
"preview_today": "Oggi:",
"preview_this_week": "Questa settimana:",
"preview_older": "Più vecchio:"
}, },
"time_format": { "time_format": {
"label": "Formato ora", "label": "Formato ora",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "編集を続ける", "keep_editing": "編集を続ける",
"tabs": { "tabs": {
"appearance": "外観", "appearance": "外観",
"language": "言語地域", "language": "言語地域、時刻",
"email": "メール動作", "email": "メール動作",
"composer": "作成", "composer": "作成",
"privacy": "プライバシーとセキュリティ", "privacy": "プライバシーとセキュリティ",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "言語地域", "title": "言語地域、時刻",
"description": "言語地域設定を構成", "description": "言語、日付形式、時刻形式、その他の地域設定",
"language": { "language": {
"label": "言語", "label": "言語",
"description": "お好みの言語を選択", "description": "お好みの言語を選択",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "日付形式", "label": "日付形式",
"description": "日付の表示形式", "description": "メール一覧での日付の表示方法",
"regional": "地域設定", "smart": "スマート(地域に合わせる)",
"iso": "ISO 8601", "relative": "相対表示(1時間前、2日前)",
"custom": "カスタム" "full": "常に完全な日付",
"preview_today": "今日:",
"preview_this_week": "今週:",
"preview_older": "それ以前:"
}, },
"time_format": { "time_format": {
"label": "時刻形式", "label": "時刻形式",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "계속 수정하기", "keep_editing": "계속 수정하기",
"tabs": { "tabs": {
"appearance": "화면 설정", "appearance": "화면 설정",
"language": "언어 지역", "language": "언어, 지역 및 시간",
"email": "메일 동작", "email": "메일 동작",
"composer": "메일 쓰기", "composer": "메일 쓰기",
"privacy": "개인정보 및 보안", "privacy": "개인정보 및 보안",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "언어 지역", "title": "언어, 지역 및 시간",
"description": "언어와 지역 형식을 설정해 주세요", "description": "언어, 날짜 형식, 시간 형식 및 기타 지역 환경설정",
"language": { "language": {
"label": "언어", "label": "언어",
"description": "사용할 언어를 선택해 주세요", "description": "사용할 언어를 선택해 주세요",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "날짜 형식", "label": "날짜 형식",
"description": "날짜가 표시되는 방식을 설정해요", "description": "이메일 목록에 날짜가 표시되는 방식",
"regional": "지역 설정", "smart": "스마트 (지역에 맞춤)",
"iso": "ISO 8601", "relative": "상대 시간 (1시간 전, 2일 전)",
"custom": "사용자 지정" "full": "항상 전체 날짜",
"preview_today": "오늘:",
"preview_this_week": "이번 주:",
"preview_older": "이전:"
}, },
"time_format": { "time_format": {
"label": "시간 형식", "label": "시간 형식",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Turpināt rediģēšanu", "keep_editing": "Turpināt rediģēšanu",
"tabs": { "tabs": {
"appearance": "Izskats", "appearance": "Izskats",
"language": "Valoda un reģions", "language": "Valoda, reģions un laiks",
"email": "Pasta darbība", "email": "Pasta darbība",
"composer": "Redaktors", "composer": "Redaktors",
"privacy": "Privātums un drošība", "privacy": "Privātums un drošība",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Valoda un reģions", "title": "Valoda, reģions un laiks",
"description": "Iestatiet valodas un reģionālās preferences", "description": "Valoda, datuma formāts, laika formāts un citas reģionālās preferences",
"language": { "language": {
"label": "Valoda", "label": "Valoda",
"description": "Izvēlieties vēlamo valodu", "description": "Izvēlieties vēlamo valodu",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Datuma formāts", "label": "Datuma formāts",
"description": "Kā attēlot datumus", "description": "Kā datumi tiek attēloti e-pasta sarakstā",
"regional": "Reģionālais", "smart": "Gudrs (atbilstoši reģionam)",
"iso": "ISO 8601", "relative": "Relatīvs (pirms 1 st., pirms 2 d.)",
"custom": "Pielāgots" "full": "Vienmēr pilns datums",
"preview_today": "Šodien:",
"preview_this_week": "Šajā nedēļā:",
"preview_older": "Vecāks:"
}, },
"time_format": { "time_format": {
"label": "Laika formāts", "label": "Laika formāts",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Doorgaan met bewerken", "keep_editing": "Doorgaan met bewerken",
"tabs": { "tabs": {
"appearance": "Uiterlijk", "appearance": "Uiterlijk",
"language": "Taal & Regio", "language": "Taal, Regio & Tijd",
"email": "E-mailgedrag", "email": "E-mailgedrag",
"composer": "Opstellen", "composer": "Opstellen",
"privacy": "Privacy & Beveiliging", "privacy": "Privacy & Beveiliging",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Taal & Regio", "title": "Taal, Regio & Tijd",
"description": "Configureer taal- en regiovoorkeuren", "description": "Taal, datumnotatie, tijdnotatie en andere regionale voorkeuren",
"language": { "language": {
"label": "Taal", "label": "Taal",
"description": "Kies je voorkeurstaal", "description": "Kies je voorkeurstaal",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Datumnotatie", "label": "Datumnotatie",
"description": "Hoe datums moeten worden weergegeven", "description": "Hoe datums worden weergegeven in de e-maillijst",
"regional": "Regionaal", "smart": "Slim (regionaal)",
"iso": "ISO 8601", "relative": "Relatief (1 u geleden, 2 d geleden)",
"custom": "Aangepast" "full": "Altijd volledige datum",
"preview_today": "Vandaag:",
"preview_this_week": "Deze week:",
"preview_older": "Ouder:"
}, },
"time_format": { "time_format": {
"label": "Tijdnotatie", "label": "Tijdnotatie",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Kontynuuj edycję", "keep_editing": "Kontynuuj edycję",
"tabs": { "tabs": {
"appearance": "Wygląd", "appearance": "Wygląd",
"language": "Język i region", "language": "Język, region i czas",
"email": "Zachowanie poczty e-mail", "email": "Zachowanie poczty e-mail",
"composer": "Redagowanie", "composer": "Redagowanie",
"privacy": "Prywatność i bezpieczeństwo", "privacy": "Prywatność i bezpieczeństwo",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Język i region", "title": "Język, region i czas",
"description": "Skonfiguruj preferencje językowe i regionalne", "description": "Język, format daty, format godziny i inne preferencje regionalne",
"language": { "language": {
"label": "Język", "label": "Język",
"description": "Wybierz preferowany język", "description": "Wybierz preferowany język",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Format daty", "label": "Format daty",
"description": "Jak mają być wyświetlane daty", "description": "Jak daty są wyświetlane na liście e-maili",
"regional": "Regionalny", "smart": "Inteligentny (regionalny)",
"iso": "ISO 8601", "relative": "Względny (1 g temu, 2 d temu)",
"custom": "Niestandardowy" "full": "Zawsze pełna data",
"preview_today": "Dzisiaj:",
"preview_this_week": "W tym tygodniu:",
"preview_older": "Starsze:"
}, },
"time_format": { "time_format": {
"label": "Format czasu", "label": "Format czasu",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Continuar editando", "keep_editing": "Continuar editando",
"tabs": { "tabs": {
"appearance": "Aparência", "appearance": "Aparência",
"language": "Idioma e Região", "language": "Idioma, Região e Hora",
"email": "Comportamento de E-mail", "email": "Comportamento de E-mail",
"composer": "Editor", "composer": "Editor",
"privacy": "Privacidade e Segurança", "privacy": "Privacidade e Segurança",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Idioma e Região", "title": "Idioma, Região e Hora",
"description": "Configure preferências de idioma e região", "description": "Idioma, formato de data, formato de hora e outras preferências regionais",
"language": { "language": {
"label": "Idioma", "label": "Idioma",
"description": "Escolha seu idioma preferido", "description": "Escolha seu idioma preferido",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Formato de Data", "label": "Formato de Data",
"description": "Como as datas devem ser exibidas", "description": "Como as datas são mostradas na lista de e-mails",
"regional": "Regional", "smart": "Inteligente (regional)",
"iso": "ISO 8601", "relative": "Relativo (há 1 h, há 2 d)",
"custom": "Personalizado" "full": "Sempre data completa",
"preview_today": "Hoje:",
"preview_this_week": "Esta semana:",
"preview_older": "Mais antigo:"
}, },
"time_format": { "time_format": {
"label": "Formato de Hora", "label": "Formato de Hora",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Продолжить редактирование", "keep_editing": "Продолжить редактирование",
"tabs": { "tabs": {
"appearance": "Внешний вид", "appearance": "Внешний вид",
"language": "Язык и регион", "language": "Язык, регион и время",
"email": "Поведение почты", "email": "Поведение почты",
"composer": "Редактор", "composer": "Редактор",
"privacy": "Конфиденциальность и безопасность", "privacy": "Конфиденциальность и безопасность",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Язык и регион", "title": "Язык, регион и время",
"description": "Настройте языковые и региональные предпочтения", "description": "Язык, формат даты, формат времени и другие региональные настройки",
"language": { "language": {
"label": "Язык", "label": "Язык",
"description": "Выберите предпочтительный язык", "description": "Выберите предпочтительный язык",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Формат даты", "label": "Формат даты",
"description": "Как отображать даты", "description": "Как отображаются даты в списке писем",
"regional": "Региональный", "smart": "Умный (по региону)",
"iso": "ISO 8601", "relative": "Относительный (1 ч назад, 2 д назад)",
"custom": "Пользовательский" "full": "Всегда полная дата",
"preview_today": "Сегодня:",
"preview_this_week": "На этой неделе:",
"preview_older": "Старее:"
}, },
"time_format": { "time_format": {
"label": "Формат времени", "label": "Формат времени",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Düzenlemeye devam et", "keep_editing": "Düzenlemeye devam et",
"tabs": { "tabs": {
"appearance": "Görünüm", "appearance": "Görünüm",
"language": "Dil ve Bölge", "language": "Dil, Bölge ve Saat",
"email": "E-posta Davranışı", "email": "E-posta Davranışı",
"composer": "Yazıcı", "composer": "Yazıcı",
"privacy": "Gizlilik ve Güvenlik", "privacy": "Gizlilik ve Güvenlik",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Dil ve Bölge", "title": "Dil, Bölge ve Saat",
"description": "Dil ve bölgesel tercihleri yapılandırın", "description": "Dil, tarih biçimi, saat biçimi ve diğer bölgesel tercihler",
"language": { "language": {
"label": "Dil", "label": "Dil",
"description": "Tercih ettiğiniz dili seçin", "description": "Tercih ettiğiniz dili seçin",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Tarih Biçimi", "label": "Tarih Biçimi",
"description": "Tarihlerin nasıl görüntüleneceği", "description": "Tarihlerin e-posta listesinde nasıl gösterileceği",
"regional": "Bölgesel", "smart": "Akıllı (bölgeye duyarlı)",
"iso": "ISO 8601", "relative": "Göreceli (1 sa önce, 2 g önce)",
"custom": "Özel" "full": "Her zaman tam tarih",
"preview_today": "Bugün:",
"preview_this_week": "Bu hafta:",
"preview_older": "Daha eski:"
}, },
"time_format": { "time_format": {
"label": "Saat Biçimi", "label": "Saat Biçimi",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "Продовжуйте редагувати", "keep_editing": "Продовжуйте редагувати",
"tabs": { "tabs": {
"appearance": "Зовнішній вигляд", "appearance": "Зовнішній вигляд",
"language": "Мова та регіон", "language": "Мова, регіон і час",
"email": "Поведінка електронної пошти", "email": "Поведінка електронної пошти",
"composer": "Композитор", "composer": "Композитор",
"privacy": "Конфіденційність і безпека", "privacy": "Конфіденційність і безпека",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "Мова та регіон", "title": "Мова, регіон і час",
"description": "Налаштуйте мовні та регіональні параметри", "description": "Мова, формат дати, формат часу та інші регіональні налаштування",
"language": { "language": {
"label": "Мова", "label": "Мова",
"description": "Виберіть бажану мову", "description": "Виберіть бажану мову",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "Формат дати", "label": "Формат дати",
"description": "Як мають відображатися дати", "description": "Як відображаються дати у списку листів",
"regional": "Регіональний", "smart": "Розумний (за регіоном)",
"iso": "ISO 8601", "relative": "Відносний (1 год тому, 2 дн тому)",
"custom": "Довільний" "full": "Завжди повна дата",
"preview_today": "Сьогодні:",
"preview_this_week": "Цього тижня:",
"preview_older": "Старіше:"
}, },
"time_format": { "time_format": {
"label": "Формат часу", "label": "Формат часу",
+10 -7
View File
@@ -752,7 +752,7 @@
"keep_editing": "继续编辑", "keep_editing": "继续编辑",
"tabs": { "tabs": {
"appearance": "外观", "appearance": "外观",
"language": "语言地区", "language": "语言地区与时间",
"email": "邮件行为", "email": "邮件行为",
"composer": "邮件撰写", "composer": "邮件撰写",
"privacy": "隐私与安全", "privacy": "隐私与安全",
@@ -932,8 +932,8 @@
} }
}, },
"language_region": { "language_region": {
"title": "语言地区", "title": "语言地区与时间",
"description": "配置语言和区域首选项", "description": "语言、日期格式、时间格式以及其他区域设置",
"language": { "language": {
"label": "语言", "label": "语言",
"description": "选择您的首选语言", "description": "选择您的首选语言",
@@ -942,10 +942,13 @@
}, },
"date_format": { "date_format": {
"label": "日期格式", "label": "日期格式",
"description": "日期应如何显示", "description": "电子邮件列表中日期的显示方式",
"regional": "区域格式", "smart": "智能(按地区)",
"iso": "ISO 8601", "relative": "相对时间(1 小时前、2 天前)",
"custom": "自定义" "full": "始终完整日期",
"preview_today": "今天:",
"preview_this_week": "本周:",
"preview_older": "更早:"
}, },
"time_format": { "time_format": {
"label": "时间格式", "label": "时间格式",
+9 -3
View File
@@ -31,7 +31,7 @@ export type ListDensity = Density;
export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent'; export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent';
export type ReplyMode = 'reply' | 'replyAll'; export type ReplyMode = 'reply' | 'replyAll';
export type SignaturePosition = 'above_quote' | 'below_quote'; export type SignaturePosition = 'above_quote' | 'below_quote';
export type DateFormat = 'regional' | 'iso' | 'custom'; export type DateFormat = 'smart' | 'relative' | 'full';
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';
@@ -300,7 +300,7 @@ const DEFAULT_SETTINGS = {
animationsEnabled: true, animationsEnabled: true,
// Language & Region // Language & Region
dateFormat: 'regional' as DateFormat, dateFormat: 'smart' as DateFormat,
timeFormat: '24h' as TimeFormat, timeFormat: '24h' as TimeFormat,
firstDayOfWeek: 1 as FirstDayOfWeek, // Monday firstDayOfWeek: 1 as FirstDayOfWeek, // Monday
@@ -760,7 +760,7 @@ export const useSettingsStore = create<SettingsState>()(
}), }),
{ {
name: 'settings-storage', name: 'settings-storage',
version: 3, version: 4,
migrate: (persisted, version) => { migrate: (persisted, version) => {
const state = persisted as Record<string, unknown>; const state = persisted as Record<string, unknown>;
if (version < 2 && state.listDensity) { if (version < 2 && state.listDensity) {
@@ -771,6 +771,12 @@ export const useSettingsStore = create<SettingsState>()(
state.protocolOpenMode = state.protocolMailtoOpenMode; state.protocolOpenMode = state.protocolMailtoOpenMode;
} }
delete state.protocolMailtoOpenMode; delete state.protocolMailtoOpenMode;
// v4: `dateFormat` was repurposed from 'regional'|'iso'|'custom' to
// 'smart'|'relative'|'full'. The old setting was never read anywhere,
// so every persisted value maps to the new default.
if (version < 4) {
state.dateFormat = 'smart';
}
return state as unknown as SettingsState; return state as unknown as SettingsState;
}, },
onRehydrateStorage: () => { onRehydrateStorage: () => {