feat(settings): add opt-in calendar localization

This commit is contained in:
NimBold
2026-07-23 03:08:52 +03:30
parent 3f298715a6
commit 5463d27ed3
18 changed files with 258 additions and 24 deletions
+12 -3
View File
@@ -4,6 +4,8 @@ import { Play, Pause, MoreVertical, Clock, ArrowUp, ArrowDown, GripVertical } fr
import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem';
import { canPauseDownload, canStartDownload } from '../utils/downloadActions';
import { useTranslation } from 'react-i18next';
import { useSettingsStore } from '../store/useSettingsStore';
import { formatDateTime } from '../utils/dateTime';
import {
downloadProgressColorClass,
formatDownloadTotal,
@@ -59,7 +61,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
onQueueDragStart,
onClick,
}) => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const calendarPreference = useSettingsStore(state => state.calendarPreference);
const liveProgress = useDownloadProgressStore(state => state.progressMap[download.id]);
const rowRef = React.useRef<HTMLDivElement>(null);
const [isRowHovered, setIsRowHovered] = React.useState(false);
@@ -71,6 +74,12 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
const normalized = download.mediaQuality.replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
return normalized.length > 0 && normalized.length <= 48 ? normalized : undefined;
})();
const dateAddedLabel = download.dateAdded
? formatDateTime(download.dateAdded, {
locale: i18n.language,
calendar: calendarPreference
})
: '-';
const updateActionPosition = React.useCallback(() => {
const row = rowRef.current;
@@ -327,9 +336,9 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
<div className="download-column-cell download-cell-right download-column-date-added" style={columnStyle('Date Added')}>
<span
className="download-cell-content download-date-value tabular-nums"
title={download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
title={dateAddedLabel}
>
{download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'}
{dateAddedLabel}
</span>
</div>
),
+15 -9
View File
@@ -18,19 +18,25 @@ import {
} from '../utils/downloadProgress';
import { resolveDownloadConnections } from '../utils/downloads';
import { useTranslation } from 'react-i18next';
import { formatDateTime, type CalendarPreference } from '../utils/dateTime';
type LoginMode = 'matching' | 'custom' | 'none';
const formatLastTry = (value?: string): string => {
const formatLastTry = (
value: string | undefined,
locale: string,
calendar: CalendarPreference
): string => {
if (!value) return '-';
const date = new Date(value);
return Number.isNaN(date.getTime())
? '-'
: date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
return formatDateTime(value, {
locale,
calendar,
options: { dateStyle: 'medium', timeStyle: 'short' }
});
};
export const PropertiesModal = () => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const categoryLabel = (category: string) => {
switch (category) {
case 'Musics': return t($ => $.navigation.categories.musics);
@@ -55,7 +61,7 @@ export const PropertiesModal = () => {
: undefined
));
const { baseDownloadFolder, perServerConnections } = useSettingsStore();
const { baseDownloadFolder, perServerConnections, calendarPreference } = useSettingsStore();
// Form states
const [url, setUrl] = useState('');
@@ -373,9 +379,9 @@ export const PropertiesModal = () => {
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium shrink-0 whitespace-nowrap">{t($ => $.properties.connections)}</span><span className="text-text-secondary truncate whitespace-nowrap" title={item.connections !== undefined ? t($ => $.properties.savedTooltip) : t($ => $.properties.defaultTooltip)}><bdi>{connectionStatus}</bdi></span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[60px] shrink-0">{t($ => $.properties.speedCap)}</span><span className="text-text-secondary truncate">{item.speedLimit || '-'}</span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[55px] shrink-0">{t($ => $.properties.category)}</span><span className="text-text-secondary truncate">{categoryLabel(item.category)}</span></div>
<div className="flex gap-1.5"><span className="text-text-muted font-medium w-[50px]">{t($ => $.properties.lastTry)}</span><span className="text-text-secondary truncate">{formatLastTry(item.lastTry)}</span></div>
<div className="flex gap-1.5"><span className="text-text-muted font-medium w-[50px]">{t($ => $.properties.lastTry)}</span><span className="text-text-secondary truncate">{formatLastTry(item.lastTry, i18n.language, calendarPreference)}</span></div>
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[90px]">{t($ => $.properties.dateAdded)}</span><span className="text-text-secondary truncate">{new Date(item.dateAdded).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}</span></div>
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[90px]">{t($ => $.properties.dateAdded)}</span><span className="text-text-secondary truncate">{formatDateTime(item.dateAdded, { locale: i18n.language, calendar: calendarPreference, options: { dateStyle: 'medium', timeStyle: 'short' } })}</span></div>
<div className="flex gap-1.5 col-span-2"><span className="text-text-muted font-medium w-[70px]">{t($ => $.properties.destination)}</span><span className="text-text-secondary truncate" title={saveLocation}>{saveLocation || baseDownloadFolder}</span></div>
{item.lastError && (item.status === 'failed' || item.status === 'retrying') && (
<div className="flex gap-1.5 col-span-4 min-w-0">
+14 -8
View File
@@ -11,6 +11,7 @@ import { WindowDragRegion } from './WindowDragRegion';
import { useToast } from '../contexts/ToastContext';
import { usePlatformInfo } from '../utils/platform';
import { useTranslation } from 'react-i18next';
import { formatDateTime } from '../utils/dateTime';
const days = [
{ value: 0, key: 'su' },
@@ -54,9 +55,10 @@ function nextScheduledRun(settings: SchedulerSettings): Date | 'disabled' | 'non
}
export default function SchedulerView() {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const savedSettings = useSettingsStore(state => state.scheduler);
const schedulerRunning = useSettingsStore(state => state.schedulerRunning);
const calendarPreference = useSettingsStore(state => state.calendarPreference);
const setScheduler = useSettingsStore(state => state.setScheduler);
const queues = useDownloadStore(state => state.queues);
const [draft, setDraft] = useState<SchedulerSettings>(savedSettings);
@@ -75,14 +77,18 @@ export default function SchedulerView() {
const scheduledRun = nextScheduledRun(draft);
if (scheduledRun === 'disabled') return t($ => $.scheduler.disabled);
if (scheduledRun === 'none') return t($ => $.scheduler.noScheduledDay);
return scheduledRun.toLocaleString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
return formatDateTime(scheduledRun, {
locale: i18n.language,
calendar: calendarPreference,
options: {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
}
});
}, [draft, t]);
}, [calendarPreference, draft, i18n.language, t]);
const hasUnsavedChanges = useMemo(
() => JSON.stringify(draft) !== JSON.stringify(savedSettings),
[draft, savedSettings]
+20
View File
@@ -693,6 +693,11 @@ runEngineChecks(false);
{ value: 'uk', label: t($ => $.settings.lookAndFeel.languageUkrainian) },
{ value: 'ru', label: t($ => $.settings.lookAndFeel.languageRussian) },
] as const;
const calendarOptions = [
{ value: 'gregorian', label: t($ => $.settings.lookAndFeel.calendarGregorian) },
{ value: 'persian', label: t($ => $.settings.lookAndFeel.calendarPersian) },
{ value: 'hebrew', label: t($ => $.settings.lookAndFeel.calendarHebrew) },
] as const;
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
const active = activeTab === type;
@@ -870,6 +875,21 @@ runEngineChecks(false);
))}
</select>
</div>
<div className="mac-settings-row">
<div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.calendar)}</span>
<small>{t($ => $.settings.lookAndFeel.calendarDescription)}</small>
</div>
<select
value={settings.calendarPreference}
onChange={(event) => settings.setCalendarPreference(event.target.value as typeof settings.calendarPreference)}
className="app-control w-48"
>
{calendarOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
<div className="mac-settings-row">
<div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.sidebarPosition)}</span>