diff --git a/web/src/lib/components/album-page/AlbumSummary.svelte b/web/src/lib/components/album-page/AlbumSummary.svelte index 3e6e160c9c..dd84bbec98 100644 --- a/web/src/lib/components/album-page/AlbumSummary.svelte +++ b/web/src/lib/components/album-page/AlbumSummary.svelte @@ -3,15 +3,18 @@ import type { AlbumResponseDto } from '@immich/sdk'; import { t } from 'svelte-i18n'; - interface Props { + type Props = { album: AlbumResponseDto; - } + }; - let { album }: Props = $props(); + const { album }: Props = $props(); + const startDate = album.startDate; - {getAlbumDateRange(album)} - + {#if startDate} + {getAlbumDateRange(startDate, album.endDate ?? startDate)} + + {/if} {$t('items_count', { values: { count: album.assetCount } })} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 3bb1539e0c..f8f3394138 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -34,6 +34,12 @@ export const dateFormats = { month: 'short', day: 'numeric', year: 'numeric', + timeZone: 'UTC', + } satisfies Intl.DateTimeFormatOptions, + albumShort: { + month: 'short', + year: 'numeric', + timeZone: 'UTC', } satisfies Intl.DateTimeFormatOptions, settings: { month: 'short', diff --git a/web/src/lib/utils/date-time.spec.ts b/web/src/lib/utils/date-time.spec.ts index f91a74c6b9..6cdf63fcd2 100644 --- a/web/src/lib/utils/date-time.spec.ts +++ b/web/src/lib/utils/date-time.spec.ts @@ -1,66 +1,93 @@ import { writable } from 'svelte/store'; +import { locale } from '$lib/stores/preferences.store'; import { getAlbumDateRange, getShortDateRange } from './date-time'; +vitest.mock('$lib/stores/preferences.store', () => ({ + locale: writable('en'), +})); + describe('getShortDateRange', () => { beforeEach(() => { vi.stubEnv('TZ', 'UTC'); + locale.set('en'); + }); + + afterAll(() => { + vi.unstubAllEnvs(); + locale.set('en'); + }); + + it('should correctly return long month if start and end date are within the same month', () => { + expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('January 2022'); + }); + + it('should correctly return month range if start and end date are in separate months within the same year', () => { + expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan – Feb 2022'); + }); + + it('should correctly return range if start and end date are in separate months and years', () => { + expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022'); + }); + + it('should correctly return long month if start and end date are within the same month, ignoring local time zone', () => { + vi.stubEnv('TZ', 'UTC+6'); + expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('January 2022'); + }); + + it('should correctly return month range if start and end date are in separate months within the same year, ignoring local time zone', () => { + vi.stubEnv('TZ', 'UTC+6'); + expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan – Feb 2022'); + }); + + it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => { + vi.stubEnv('TZ', 'UTC+6'); + expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022'); + }); + + it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => { + vi.stubEnv('TZ', 'UTC-6'); + expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022'); + }); + + it('should use the correct locale to return month range', () => { + locale.set('fr'); + expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('janv.–févr. 2022'); + }); + + it('should use the correct locale to return month-year range', () => { + locale.set('fr'); + expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('déc. 2021 – janv. 2022'); + }); +}); + +describe('getAlbumDateRange', () => { + beforeAll(() => { + vi.stubEnv('TZ', 'UTC'); }); afterAll(() => { vi.unstubAllEnvs(); }); - it('should correctly return month if start and end date are within the same month', () => { - expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('Jan 2022'); + it('should work', () => { + expect(getAlbumDateRange('2021-01-01T00:00:00Z', '2021-01-05T00:00:00Z')).toEqual('Jan 1 – 5, 2021'); }); - it('should correctly return month range if start and end date are in separate months within the same year', () => { - expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan - Feb 2022'); + it('should work with a single day range', () => { + expect(getAlbumDateRange('2021-01-01T09:00:00Z', '2021-01-01T10:00:00Z')).toEqual('Jan 1, 2021'); }); - it('should correctly return range if start and end date are in separate months and years', () => { - expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 - Jan 2022'); + it('should work with positive time zone present', () => { + expect(getAlbumDateRange('2021-01-01T00:00:00+05:00', '2021-01-01T00:00:00+05:00')).toEqual('Jan 1, 2021'); }); - it('should correctly return month if start and end date are within the same month, ignoring local time zone', () => { - vi.stubEnv('TZ', 'UTC+6'); - expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('Jan 2022'); + it('should work with negative time zone present', () => { + expect(getAlbumDateRange('2021-01-01T00:00:00-05:00', '2021-01-01T00:00:00-05:00')).toEqual('Jan 1, 2021'); }); - it('should correctly return month range if start and end date are in separate months within the same year, ignoring local time zone', () => { - vi.stubEnv('TZ', 'UTC+6'); - expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan - Feb 2022'); - }); - - it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => { - vi.stubEnv('TZ', 'UTC+6'); - expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 - Jan 2022'); - }); -}); - -describe('getAlbumDate', () => { - beforeAll(() => { - process.env.TZ = 'UTC'; - - vitest.mock('$lib/stores/preferences.store', () => ({ - locale: writable('en'), - })); - }); - - it('should work with only a start date', () => { - expect(getAlbumDateRange({ startDate: '2021-01-01T00:00:00Z' })).toEqual('Jan 1, 2021'); - }); - - it('should work with a start and end date', () => { - expect( - getAlbumDateRange({ - startDate: '2021-01-01T00:00:00Z', - endDate: '2021-01-05T00:00:00Z', - }), - ).toEqual('Jan 1, 2021 - Jan 5, 2021'); - }); - - it('should work with the new date format', () => { - expect(getAlbumDateRange({ startDate: '2021-01-01T00:00:00+05:00' })).toEqual('Jan 1, 2021'); + it('should use the proper locale', () => { + locale.set('fr'); + expect(getAlbumDateRange('2020-03-26T12:00:00Z', '2021-12-01T00:00:00Z')).toEqual('26 mars 2020 – 1 déc. 2021'); + locale.set('en'); }); }); diff --git a/web/src/lib/utils/date-time.ts b/web/src/lib/utils/date-time.ts index d06f92376b..42f8182c19 100644 --- a/web/src/lib/utils/date-time.ts +++ b/web/src/lib/utils/date-time.ts @@ -7,69 +7,28 @@ export function parseUtcDate(date: string) { return DateTime.fromISO(date, { zone: 'UTC' }).toUTC(); } -export const getShortDateRange = (startTimestamp: string, endTimestamp: string) => { +const getDateRange = (startTimestamp: string, endTimestamp: string, format: 'short' | 'long') => { const userLocale = get(locale); - let startDate = DateTime.fromISO(startTimestamp).setZone('UTC'); - let endDate = DateTime.fromISO(endTimestamp).setZone('UTC'); + const startDate = DateTime.fromISO(startTimestamp).setZone('UTC'); + const endDate = DateTime.fromISO(endTimestamp).setZone('UTC').setLocale(userLocale); - if (userLocale) { - startDate = startDate.setLocale(userLocale); - endDate = endDate.setLocale(userLocale); + if (startDate.year === endDate.year && startDate.month === endDate.month && format === 'short') { + return endDate.toLocaleString({ month: 'long', year: 'numeric' }); } - const endDateLocalized = endDate.toLocaleString({ - month: 'short', - year: 'numeric', - }); - - if (startDate.year === endDate.year) { - if (startDate.month === endDate.month) { - // Same year and month. - // e.g.: aug. 2024 - return endDateLocalized; - } else { - // Same year but different month. - // e.g.: jul. - sept. 2024 - const startMonthLocalized = startDate.toLocaleString({ - month: 'short', - }); - return `${startMonthLocalized} - ${endDateLocalized}`; - } - } else { - // Different year. - // e.g.: feb. 2021 - sept. 2024 - const startDateLocalized = startDate.toLocaleString({ - month: 'short', - year: 'numeric', - }); - return `${startDateLocalized} - ${endDateLocalized}`; - } + const formatter = new Intl.DateTimeFormat( + userLocale, + format === 'short' ? dateFormats.albumShort : dateFormats.album, + ); + return formatter.formatRange(startDate.toJSDate(), endDate.toJSDate()); }; -const formatDate = (date?: string) => { - if (!date) { - return; - } +/** + * Get localized date range in short format like 'Oct – Nov 2026', with full month if start and end are the same: 'October 2026' + */ +export const getShortDateRange = (start: string, end: string) => getDateRange(start, end, 'short'); - // without timezone - const localDate = date.replace(/Z$/, '').replace(/\+.+$/, ''); - return localDate ? new Date(localDate).toLocaleDateString(get(locale), dateFormats.album) : undefined; -}; - -export const getAlbumDateRange = (album: { startDate?: string; endDate?: string }) => { - const start = formatDate(album.startDate); - const end = formatDate(album.endDate); - - if (start && end && start !== end) { - return `${start} - ${end}`; - } - - if (start) { - return start; - } - - return ''; -}; +export const getAlbumDateRange = (start: string, end: string) => getDateRange(start, end, 'long'); /** * Use this to convert from "5pm EST" to "5pm UTC"