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 @@
@@ -144,7 +144,7 @@
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 @@
{
+ 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;
+ };
+});