diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index cd4010b135..966f382838 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -123,7 +123,7 @@ {getAltText(asset)} ((imageLoaded = true), (assetFileUrl = imageLoaderUrl))} on:error={() => (imageError = imageLoaded = true)} /> @@ -136,7 +136,7 @@ {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} {getAltText(asset)} @@ -144,7 +144,7 @@ {getAltText(asset)}

diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index 53a02a79ae..74d17c621d 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -32,7 +32,7 @@ {getAltText(asset)} { + beforeAll(async () => { + await init({ fallbackLocale: 'en-US' }); + register('en-US', () => import('$lib/i18n/en.json')); + await waitLocale('en-US'); + }); + + it('defaults to the description, if available', () => { + const asset = { + exifInfo: { description: 'description' }, + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual('description'); + }); + }); + + it('includes the city and country', () => { + const asset = { + exifInfo: { city: 'city', country: 'country' }, + localDateTime: '2024-01-01T12:00:00.000Z', + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual('Image taken in city, country on January 1, 2024'); + }); + }); + + // convert the people tests into an it.each + it.each([ + [[{ name: 'person' }], 'Image taken with person on January 1, 2024'], + [[{ name: 'person1' }, { name: 'person2' }], 'Image taken with person1 and person2 on January 1, 2024'], + [ + [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }], + 'Image taken with person1, person2, and person3 on January 1, 2024', + ], + [ + [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }], + 'Image taken with person1, person2, and 2 others on January 1, 2024', + ], + ])('includes people, correctly formatted', (people, expected) => { + const asset = { + localDateTime: '2024-01-01T12:00:00.000Z', + people, + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual(expected); + }); + }); + + it('handles videos, location, people, and date', () => { + const asset = { + exifInfo: { city: 'city', country: 'country' }, + localDateTime: '2024-01-01T12:00:00.000Z', + people: [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }, { name: 'person5' }], + type: AssetTypeEnum.Video, + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual('Video taken in city, country with person1, person2, and 3 others on January 1, 2024'); + }); + }); +}); diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts index d349c4e830..fef0c6dd6a 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -1,4 +1,6 @@ -import type { AssetResponseDto } from '@immich/sdk'; +import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { derived } from 'svelte/store'; import { fromLocalDateTime } from './timeline-util'; /** @@ -35,29 +37,39 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number return 300; } -export function getAltText(asset: AssetResponseDto) { - if (asset.exifInfo?.description) { - return asset.exifInfo.description; - } +export const getAltText = derived(t, ($t) => { + return (asset: AssetResponseDto) => { + if (asset.exifInfo?.description) { + return asset.exifInfo.description; + } - let altText = 'Image taken'; - if (asset.exifInfo?.city && asset.exifInfo.country) { - altText += ` in ${asset.exifInfo.city}, ${asset.exifInfo.country}`; - } + let altText = $t('image_taken', { values: { isVideo: asset.type === AssetTypeEnum.Video } }); - const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; - if (names.length == 1) { - altText += ` with ${names[0]}`; - } - if (names.length > 1 && names.length <= 3) { - altText += ` with ${names.slice(0, -1).join(', ')} and ${names.at(-1)}`; - } - if (names.length > 3) { - altText += ` with ${names.slice(0, 2).join(', ')}, and ${names.length - 2} others`; - } + if (asset.exifInfo?.city && asset.exifInfo?.country) { + const placeText = $t('image_alt_text_place', { + values: { city: asset.exifInfo.city, country: asset.exifInfo.country }, + }); + altText += ` ${placeText}`; + } - const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }); - altText += ` on ${date}`; + const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; + if (names.length > 0) { + const namesText = $t('image_alt_text_people', { + values: { + count: names.length, + person1: names[0], + person2: names[1], + person3: names[2], + others: names.length > 3 ? names.length - 2 : 0, + }, + }); + altText += ` ${namesText}`; + } - return altText; -} + const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }); + const dateText = $t('image_alt_text_date', { values: { date } }); + altText += ` ${dateText}`; + + return altText; + }; +});