diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index ca67b36435..f4f57c4427 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -711,10 +711,16 @@ "host": "Host", "hour": "Hour", "image": "Image", - "image_alt_text_date": "on {date}", - "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}", - "image_alt_text_place": "in {city}, {country}", - "image_taken": "{isVideo, select, true {Video taken} other {Image taken}}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} taken on {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} taken with {person1} on {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} taken with {person1} and {person2} on {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {person3} on {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {additionalCount, number} others on {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} on {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} on {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} and {person2} on {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {person3} on {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {additionalCount, number} others on {date}", "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web Interface", "import_from_json": "Import from JSON", diff --git a/web/src/lib/utils/thumbnail-util.spec.ts b/web/src/lib/utils/thumbnail-util.spec.ts index 79846e067d..b4dbec8752 100644 --- a/web/src/lib/utils/thumbnail-util.spec.ts +++ b/web/src/lib/utils/thumbnail-util.spec.ts @@ -2,6 +2,11 @@ import { getAltText } from '$lib/utils/thumbnail-util'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; import { init, register, waitLocale } from 'svelte-i18n'; +const onePerson = [{ name: 'person' }]; +const twoPeople = [{ name: 'person1' }, { name: 'person2' }]; +const threePeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }]; +const fourPeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }]; + describe('getAltText', () => { beforeAll(async () => { await init({ fallbackLocale: 'en-US' }); @@ -9,6 +14,44 @@ describe('getAltText', () => { await waitLocale('en-US'); }); + it.each` + isVideo | city | country | people | expected + ${false} | ${undefined} | ${'country'} | ${undefined} | ${'Image taken on January 1, 2024'} + ${true} | ${'city'} | ${undefined} | ${undefined} | ${'Video taken on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${[]} | ${'Image taken in city, country on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${[]} | ${'Video taken in city, country on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${onePerson} | ${'Image taken with person on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${twoPeople} | ${'Image taken with person1 and person2 on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${threePeople} | ${'Image taken with person1, person2, and person3 on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${fourPeople} | ${'Image taken with person1, person2, and 2 others on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${onePerson} | ${'Image taken in city, country with person on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${twoPeople} | ${'Image taken in city, country with person1 and person2 on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${threePeople} | ${'Image taken in city, country with person1, person2, and person3 on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${fourPeople} | ${'Image taken in city, country with person1, person2, and 2 others on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${onePerson} | ${'Video taken with person on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${twoPeople} | ${'Video taken with person1 and person2 on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${threePeople} | ${'Video taken with person1, person2, and person3 on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${fourPeople} | ${'Video taken with person1, person2, and 2 others on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${onePerson} | ${'Video taken in city, country with person on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${twoPeople} | ${'Video taken in city, country with person1 and person2 on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${threePeople} | ${'Video taken in city, country with person1, person2, and person3 on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${fourPeople} | ${'Video taken in city, country with person1, person2, and 2 others on January 1, 2024'} + `( + 'generates correctly formatted alt text when isVideo=$isVideo, city=$city, country=$country, people=$people.length', + ({ isVideo, city, country, people, expected }) => { + const asset = { + exifInfo: { city, country }, + localDateTime: '2024-01-01T12:00:00.000Z', + people, + type: isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual(expected); + }); + }, + ); + it('defaults to the description, if available', () => { const asset = { exifInfo: { description: 'description' }, @@ -18,51 +61,4 @@ describe('getAltText', () => { 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 fef0c6dd6a..a53691e716 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -43,33 +43,52 @@ export const getAltText = derived(t, ($t) => { return asset.exifInfo.description; } - let altText = $t('image_taken', { values: { isVideo: asset.type === AssetTypeEnum.Video } }); - - 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 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}`; - } - const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }); - const dateText = $t('image_alt_text_date', { values: { date } }); - altText += ` ${dateText}`; + const hasPlace = !!asset.exifInfo?.city && !!asset.exifInfo?.country; + const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; + const peopleCount = names.length; + const isVideo = asset.type === AssetTypeEnum.Video; - return altText; + const values = { + date, + city: asset.exifInfo?.city, + country: asset.exifInfo?.country, + person1: names[0], + person2: names[1], + person3: names[2], + isVideo, + additionalCount: peopleCount > 3 ? peopleCount - 2 : 0, + }; + + if (peopleCount > 0) { + switch (peopleCount) { + case 1: { + return hasPlace + ? $t('image_alt_text_date_place_1_person', { values }) + : $t('image_alt_text_date_1_person', { values }); + } + case 2: { + return hasPlace + ? $t('image_alt_text_date_place_2_people', { values }) + : $t('image_alt_text_date_2_people', { values }); + } + case 3: { + return hasPlace + ? $t('image_alt_text_date_place_3_people', { values }) + : $t('image_alt_text_date_3_people', { values }); + } + default: { + return hasPlace + ? $t('image_alt_text_date_place_4_or_more_people', { values }) + : $t('image_alt_text_date_4_or_more_people', { values }); + } + } + } + + if (hasPlace) { + return $t('image_alt_text_date_place', { values }); + } + + return $t('image_alt_text_date', { values }); }; });