mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
# Conflicts: # components/layout/__tests__/account-switcher-mountain-view.test.tsx # components/layout/account-switcher.tsx # components/settings/accessibility-settings.tsx # components/settings/reading-settings.tsx
175 lines
7.5 KiB
TypeScript
175 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo } from 'react';
|
||
import { useTranslations } from 'next-intl';
|
||
import { LanguageSwitcher } from '@/components/ui/language-switcher';
|
||
import { useLocaleStore } from '@/stores/locale-store';
|
||
import { useSettingsStore } from '@/stores/settings-store';
|
||
import type { DateFormat, DateLocale, TimeFormat, FirstDayOfWeek } from '@/stores/settings-store';
|
||
import { formatDate } from '@/lib/utils';
|
||
import { getEffectiveLocale } from '@/i18n/detect-locale';
|
||
import { getEffectiveTimeZone } from '@/lib/timezone';
|
||
import { useIsDesktopApp } from '@/hooks/use-is-desktop-app';
|
||
import { SettingsSection, SettingItem, Select, RadioGroup } from './settings-section';
|
||
import { TimeZonePicker } from './time-zone-picker';
|
||
|
||
export function LanguageSettings() {
|
||
const t = useTranslations('settings.language_region');
|
||
const tDays = useTranslations('calendar.days');
|
||
const tGeneral = useTranslations('uxs_general');
|
||
const desktop = useIsDesktopApp();
|
||
|
||
const { dateFormat, dateLocale, timeFormat, firstDayOfWeek, timeZone, 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 dateLocale; void timeFormat; void timeZone;
|
||
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);
|
||
// The date order samples use last year: this year's dates read "Oct 24"
|
||
// when the order follows the language.
|
||
const lastYear = new Date(older);
|
||
lastYear.setFullYear(now.getFullYear() - 1);
|
||
return {
|
||
today: formatDate(today),
|
||
thisWeek: formatDate(thisWeek),
|
||
older: formatDate(older),
|
||
// Every dropdown option carries a sample of what it renders, so the
|
||
// choices read alike instead of mixing names, hints and patterns.
|
||
dateFormat: {
|
||
smart: formatDate(thisWeek, { dateFormat: 'smart' }),
|
||
relative: formatDate(thisWeek, { dateFormat: 'relative' }),
|
||
full: formatDate(older, { dateFormat: 'full' }),
|
||
},
|
||
dateLocale: {
|
||
auto: formatDate(lastYear, { dateFormat: 'smart', dateLocale: 'auto' }),
|
||
iso: formatDate(lastYear, { dateFormat: 'smart', dateLocale: 'iso' }),
|
||
'en-GB': formatDate(lastYear, { dateFormat: 'smart', dateLocale: 'en-GB' }),
|
||
'en-US': formatDate(lastYear, { dateFormat: 'smart', dateLocale: 'en-US' }),
|
||
},
|
||
};
|
||
}, [locale, dateFormat, dateLocale, timeFormat, timeZone]);
|
||
|
||
// CJK labels use full-width parentheses.
|
||
const withExample = (label: string, example: string) => {
|
||
if (!example) return label;
|
||
return /^(zh|ja)/.test(getEffectiveLocale()) ? `${label}(${example})` : `${label} (${example})`;
|
||
};
|
||
|
||
// Current wall-clock in the effective zone so a pick can be sanity-checked.
|
||
const timeZonePreview = useMemo(() => {
|
||
void timeZone; void timeFormat; void locale;
|
||
try {
|
||
return new Date().toLocaleTimeString(locale && locale !== 'auto' ? locale : undefined, {
|
||
timeZone: getEffectiveTimeZone(),
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: timeFormat === '12h',
|
||
timeZoneName: 'short',
|
||
});
|
||
} catch {
|
||
return '';
|
||
}
|
||
}, [timeZone, timeFormat, locale]);
|
||
|
||
return (
|
||
<SettingsSection id="language" title={t('title')} description={t('description')}>
|
||
<SettingItem label={t('language.label')} description={t('language.description')}>
|
||
<LanguageSwitcher />
|
||
</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: withExample(t('date_format.smart'), preview.dateFormat.smart) },
|
||
{ value: 'relative', label: withExample(t('date_format.relative'), preview.dateFormat.relative) },
|
||
{ value: 'full', label: withExample(t('date_format.full'), preview.dateFormat.full) },
|
||
]}
|
||
/>
|
||
<div className="text-xs text-muted-foreground text-end 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 advanced label={t('date_locale.label')} description={t('date_locale.description')}>
|
||
<Select
|
||
value={dateLocale}
|
||
onChange={(value) => updateSetting('dateLocale', value as DateLocale)}
|
||
options={[
|
||
{ value: 'auto', label: withExample(t('date_locale.auto'), preview.dateLocale.auto) },
|
||
{ value: 'iso', label: withExample(t('date_locale.iso'), preview.dateLocale.iso) },
|
||
{ value: 'en-GB', label: withExample(t('date_locale.dmy'), preview.dateLocale['en-GB']) },
|
||
{ value: 'en-US', label: withExample(t('date_locale.mdy'), preview.dateLocale['en-US']) },
|
||
]}
|
||
/>
|
||
</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('time_zone.label')}
|
||
description={desktop ? tGeneral('time_zone_desktop_description') : t('time_zone.description')}
|
||
>
|
||
<div className="flex flex-col items-end gap-2">
|
||
<TimeZonePicker value={timeZone} onChange={(value) => updateSetting('timeZone', value)} />
|
||
{timeZonePreview && (
|
||
<div className="text-xs text-muted-foreground text-end font-mono">
|
||
<span className="opacity-70">{t('time_zone.preview_now')} </span>
|
||
<span className="text-foreground/90">{timeZonePreview}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</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: '6', label: tDays('saturday') },
|
||
{ value: '0', label: tDays('sunday') },
|
||
]}
|
||
/>
|
||
</SettingItem>
|
||
</SettingsSection>
|
||
);
|
||
}
|