From 8edf61f9e75b484cb585fdccf6ddfb53ab506510 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 15 Dec 2025 20:20:32 -0800 Subject: [PATCH] localize dates (#1237) --- src/i18n/locales/en.json | 6 + .../components/sidebar-playlist-list.tsx | 4 +- src/renderer/utils/format.tsx | 116 ++++++++++++++---- 3 files changed, 99 insertions(+), 27 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 077f655b8..330a5b325 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -267,6 +267,12 @@ "trackNumber": "track", "explicitStatus": "$t(common.explicitStatus)" }, + "datetime": { + "minuteShort": "min", + "secondShort": "sec", + "hourShort": "hr", + "dayShort": "day" + }, "filterOperator": { "after": "is after", "afterDate": "is after (date)", diff --git a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx index 884189406..9de96f865 100644 --- a/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx +++ b/src/renderer/features/sidebar/components/sidebar-playlist-list.tsx @@ -20,7 +20,7 @@ import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-b import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/renderer/store'; -import { formatDurationStringShort } from '/@/renderer/utils'; +import { formatDurationString } from '/@/renderer/utils'; import { Accordion } from '/@/shared/components/accordion/accordion'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon'; import { ButtonProps } from '/@/shared/components/button/button'; @@ -194,7 +194,7 @@ const PlaylistRowButton = memo(({ item, name, onContextMenu, to }: PlaylistRowBu
- {formatDurationStringShort(item.duration ?? 0)} + {formatDurationString(item.duration ?? 0)}
{item.ownerId === permissions.userId && Boolean(item.public) && ( diff --git a/src/renderer/utils/format.tsx b/src/renderer/utils/format.tsx index 135412360..2f00b4ee0 100644 --- a/src/renderer/utils/format.tsx +++ b/src/renderer/utils/format.tsx @@ -1,54 +1,118 @@ import type { Album, AlbumArtist, Song } from '/@/shared/types/domain-types'; import dayjs from 'dayjs'; +import 'dayjs/locale/ar'; +import 'dayjs/locale/ca'; +import 'dayjs/locale/cs'; +import 'dayjs/locale/de'; +import 'dayjs/locale/en'; +import 'dayjs/locale/es'; +import 'dayjs/locale/eu'; +import 'dayjs/locale/fa'; +import 'dayjs/locale/fi'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/hu'; +import 'dayjs/locale/id'; +import 'dayjs/locale/it'; +import 'dayjs/locale/ja'; +import 'dayjs/locale/ko'; +import 'dayjs/locale/nb'; +import 'dayjs/locale/nl'; +import 'dayjs/locale/pl'; +import 'dayjs/locale/pt'; +import 'dayjs/locale/pt-br'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/sl'; +import 'dayjs/locale/sr'; +import 'dayjs/locale/sv'; +import 'dayjs/locale/ta'; +import 'dayjs/locale/tr'; +import 'dayjs/locale/zh-cn'; +import 'dayjs/locale/zh-tw'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; import relativeTime from 'dayjs/plugin/relativeTime'; import utc from 'dayjs/plugin/utc'; import formatDuration from 'format-duration'; +import i18n from '/@/i18n/i18n'; import { Rating } from '/@/shared/components/rating/rating'; dayjs.extend(relativeTime); dayjs.extend(utc); +dayjs.extend(localizedFormat); -const FORMATS: Record = Object.freeze({ - 0: 'YYYY', - 1: 'MMM YYYY', - 2: 'MMM D, YYYY', -}); +const getDayjsLocale = (i18nLang: string): string => { + const localeMap: Record = { + ar: 'ar', + ca: 'ca', + cs: 'cs', + de: 'de', + en: 'en', + es: 'es', + eu: 'eu', + fa: 'fa', + fi: 'fi', + fr: 'fr', + hu: 'hu', + id: 'id', + it: 'it', + ja: 'ja', + ko: 'ko', + 'nb-NO': 'nb', + nl: 'nl', + pl: 'pl', + pt: 'pt', + 'pt-BR': 'pt-br', + ru: 'ru', + sl: 'sl', + sr: 'sr', + sv: 'sv', + ta: 'ta', + tr: 'tr', + 'zh-Hans': 'zh-cn', + 'zh-Hant': 'zh-tw', + }; -const getDateFormat = (key: string): string => { - const dashes = Math.min(key.split('-').length - 1, 2); - return FORMATS[dashes]; + return localeMap[i18nLang] || 'en'; }; -export const formatDateAbsolute = (key: null | string) => - key ? dayjs(key).format(getDateFormat(key)) : ''; +const updateDayjsLocale = () => { + const dayjsLocale = getDayjsLocale(i18n.language); + dayjs.locale(dayjsLocale); +}; + +// Set initial locale +updateDayjsLocale(); + +// Listen for i18n language changes +i18n.on('languageChanged', updateDayjsLocale); + +export const formatDateAbsolute = (key: null | string) => (key ? dayjs(key).format('LL') : ''); export const formatDateAbsoluteUTC = (key: null | string) => - key ? dayjs.utc(key).format(getDateFormat(key)) : ''; + key ? dayjs.utc(key).format('LL') : ''; -export const formatHrDateTime = (key: null | string) => - key ? dayjs(key).format('YYYY-MM-DD HH:mm') : ''; +export const formatHrDateTime = (key: null | string) => (key ? dayjs(key).format('LLL') : ''); export const formatDateRelative = (key: null | string) => (key ? dayjs(key).fromNow() : ''); export const formatDurationString = (duration: number) => { - const rawDuration = formatDuration(duration).split(':'); + const rawDuration = formatDuration(duration, { leading: false }).split(':'); let string; switch (rawDuration.length) { case 1: - string = `${rawDuration[0]} sec`; + string = `${rawDuration[0]} ${i18n.t('datetime.secondShort')}`; break; case 2: - string = `${rawDuration[0]} min ${rawDuration[1]} sec`; + string = `${rawDuration[0]} ${i18n.t('datetime.minuteShort')} ${rawDuration[1]} ${i18n.t('datetime.secondShort')}`; break; case 3: - string = `${rawDuration[0]} hr ${rawDuration[1]} min ${rawDuration[2]} sec`; + string = `${rawDuration[0]} ${i18n.t('datetime.hourShort')} ${rawDuration[1]} ${i18n.t('datetime.minuteShort')} ${rawDuration[2]} ${i18n.t('datetime.secondShort')}`; break; case 4: - string = `${rawDuration[0]} day ${rawDuration[1]} hr ${rawDuration[2]} min ${rawDuration[3]} sec`; + string = `${rawDuration[0]} ${i18n.t('datetime.dayShort')} ${rawDuration[1]} ${i18n.t('datetime.hourShort')} ${rawDuration[2]} ${i18n.t('datetime.minuteShort')} ${rawDuration[3]} ${i18n.t('datetime.secondShort')}`; break; } @@ -58,15 +122,17 @@ export const formatDurationString = (duration: number) => { export const formatDurationStringShort = (duration: number) => { const rawDuration = formatDuration(duration).split(':'); - if (rawDuration.length === 2) { - // Less than 1 hour: show "0h" and minutes - return `0h ${rawDuration[0]}m`; - } else if (rawDuration.length >= 3) { - // 1 hour or more: show hours and minutes - return `${rawDuration[0]}h ${rawDuration[1]}m`; + if (rawDuration.length === 4) { + return `${rawDuration[0]}${i18n.t('datetime.dayShort')} ${rawDuration[1]}${i18n.t('datetime.hourShort')}`; + } else if (rawDuration.length === 3) { + return `${rawDuration[0]}${i18n.t('datetime.hourShort')} ${rawDuration[1]}${i18n.t('datetime.minuteShort')}`; + } else if (rawDuration.length === 2) { + return `${rawDuration[0]}${i18n.t('datetime.minuteShort')} ${rawDuration[1]}${i18n.t('datetime.secondShort')}`; + } else if (rawDuration.length === 1) { + return `${rawDuration[0]}${i18n.t('datetime.secondShort')}`; } - return '0h 0m'; + return rawDuration; }; export const formatRating = (item: Album | AlbumArtist | Song) =>