diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index caef2d5cbbc5c..c39ea757177ea 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -160,7 +160,7 @@ { value: '1080', text: '1080p' }, { value: '720', text: '720p' }, { value: '480', text: '480p' }, - { value: 'original', text: 'original' }, + { value: 'original', text: $t('original') }, ]} name="resolution" isEdited={config.ffmpeg.targetResolution !== savedConfig.ffmpeg.targetResolution} @@ -191,7 +191,7 @@ bind:value={config.ffmpeg.transcode} name="transcode" options={[ - { value: TranscodePolicy.All, text: 'All videos' }, + { value: TranscodePolicy.All, text: $t('all_videos') }, { value: TranscodePolicy.Optimal, text: $t('admin.transcoding_optimal_description'), @@ -233,7 +233,7 @@ }, { value: ToneMapping.Disabled, - text: 'Disabled', + text: $t('disabled'), }, ]} isEdited={config.ffmpeg.tonemap !== savedConfig.ffmpeg.tonemap} diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 977899db821ed..6ffa273a4d256 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -2,6 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { albumFactory } from '@test-data'; import '@testing-library/jest-dom'; import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte'; +import { init, register, waitLocale } from 'svelte-i18n'; import AlbumCard from '../album-card.svelte'; const onShowContextMenu = vi.fn(); @@ -9,6 +10,12 @@ const onShowContextMenu = vi.fn(); describe('AlbumCard component', () => { let sut: RenderResult; + beforeAll(async () => { + await init({ fallbackLocale: 'en-US' }); + register('en-US', () => import('$lib/i18n/en.json')); + await waitLocale('en-US'); + }); + it.each([ { album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }), @@ -36,7 +43,7 @@ describe('AlbumCard component', () => { const albumImgElement = sut.getByTestId('album-image'); const albumNameElement = sut.getByTestId('album-name'); const albumDetailsElement = sut.getByTestId('album-details'); - const detailsText = `${count} items` + (shared ? ' . shared' : ''); + const detailsText = `${count} items` + (shared ? ' . Shared' : ''); expect(albumImgElement).toHaveAttribute('src'); expect(albumImgElement).toHaveAttribute('alt', album.albumName); diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte index d70965c9f63b0..0e731a683cbb3 100644 --- a/web/src/lib/components/album-page/album-card-group.svelte +++ b/web/src/lib/components/album-page/album-card-group.svelte @@ -9,6 +9,7 @@ import { mdiChevronRight } from '@mdi/js'; import AlbumCard from '$lib/components/album-page/album-card.svelte'; import Icon from '$lib/components/elements/icon.svelte'; + import { t } from 'svelte-i18n'; export let albums: AlbumResponseDto[]; export let group: AlbumGroup | undefined = undefined; @@ -41,7 +42,7 @@ class="inline-block -mt-2.5 transition-all duration-[250ms] {iconRotation}" /> {group.name} - ({albums.length} {albums.length > 1 ? 'albums' : 'album'}) + ({$t('albums_count', { values: { count: albums.length } })})
diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index b5369337387c3..60eda166ab934 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -1,5 +1,4 @@ @@ -118,8 +154,8 @@ options={Object.values(sortOptionsMetadata)} selectedOption={selectedSortOption} on:select={({ detail }) => handleChangeSortBy(detail)} - render={({ text }) => ({ - title: text, + render={({ id }) => ({ + title: albumSortByNames[id], icon: sortIcon, })} /> @@ -130,8 +166,8 @@ options={Object.values(groupOptionsMetadata)} selectedOption={selectedGroupOption} on:select={({ detail }) => handleChangeGroupBy(detail)} - render={({ text, isDisabled }) => ({ - title: text, + render={({ id, isDisabled }) => ({ + title: albumGroupByNames[id], icon: groupIcon, disabled: isDisabled(), })} diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 1f5d654c4a895..740b3876bd6e5 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -304,7 +304,7 @@ const isConfirmed = await dialogController.show({ id: 'delete-album', - prompt: `Are you sure you want to delete the album ${albumToDelete.albumName}?\nIf this album is shared, other users will not be able to access it anymore.`, + prompt: $t('album_delete_confirmation', { values: { album: albumToDelete.albumName } }), }); if (!isConfirmed) { @@ -340,7 +340,7 @@ message: $t('album_info_updated'), type: NotificationType.Info, button: { - text: 'View Album', + text: $t('view_album'), onClick() { return goto(`${AppRoute.ALBUMS}/${album.id}`); }, diff --git a/web/src/lib/components/album-page/albums-table-header.svelte b/web/src/lib/components/album-page/albums-table-header.svelte index 527b1822e0b0f..2c396bebed7dc 100644 --- a/web/src/lib/components/album-page/albums-table-header.svelte +++ b/web/src/lib/components/album-page/albums-table-header.svelte @@ -1,6 +1,7 @@ @@ -27,6 +39,6 @@ ↑ {/if} {/if} - {option.text} + {albumSortByNames[option.id]} diff --git a/web/src/lib/components/album-page/albums-table-row.svelte b/web/src/lib/components/album-page/albums-table-row.svelte index b85b9d1229508..f91628ec63912 100644 --- a/web/src/lib/components/album-page/albums-table-row.svelte +++ b/web/src/lib/components/album-page/albums-table-row.svelte @@ -34,7 +34,9 @@ path={mdiShareVariantOutline} size="16" class="inline ml-1 opacity-70" - title={album.ownerId === $user.id ? $t('shared_by_you') : `Shared by ${album.owner.name}`} + title={album.ownerId === $user.id + ? $t('shared_by_you') + : $t('shared_by_user', { values: { user: album.owner.name } })} /> {/if} diff --git a/web/src/lib/components/album-page/albums-table.svelte b/web/src/lib/components/album-page/albums-table.svelte index 0c33584eb1a36..30164ef6aec61 100644 --- a/web/src/lib/components/album-page/albums-table.svelte +++ b/web/src/lib/components/album-page/albums-table.svelte @@ -13,6 +13,7 @@ sortOptionsMetadata, type AlbumGroup, } from '$lib/utils/album-utils'; + import { t } from 'svelte-i18n'; export let groupedAlbums: AlbumGroup[]; export let albumGroupOption: string = AlbumGroupBy.None; @@ -58,8 +59,7 @@ /> {albumGroup.name} - ({albumGroup.albums.length} - {albumGroup.albums.length > 1 ? 'albums' : 'album'}) + ({$t('albums_count', { values: { count: albumGroup.albums.length } })}) diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 3bda154249c51..c9ac22499271b 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -53,7 +53,10 @@ try { await removeUserFromAlbum({ id: album.id, userId }); dispatch('remove', userId); - const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`; + const message = + userId === 'me' + ? $t('album_user_left', { values: { album: album.albumName } }) + : $t('album_user_removed', { values: { user: selectedRemoveUser.name } }); notificationController.show({ type: NotificationType.Info, message }); } catch (error) { handleError(error, $t('errors.unable_to_remove_album_users')); @@ -65,7 +68,9 @@ const handleSetReadonly = async (user: UserResponseDto, role: AlbumUserRole) => { try { await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } }); - const message = `Set ${user.name} as ${role}`; + const message = $t('user_role_set', { + values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') }, + }); dispatch('refreshAlbum'); notificationController.show({ type: NotificationType.Info, message }); } catch (error) { @@ -101,9 +106,9 @@
{#if role === AlbumUserRole.Viewer} - Viewer + {$t('role_viewer')} {:else} - Editor + {$t('role_editor')} {/if}
{#if isOwned} @@ -135,8 +140,8 @@ {#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id} (selectedRemoveUser = null)} @@ -145,9 +150,9 @@ {#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id} (selectedRemoveUser = null)} /> diff --git a/web/src/lib/components/album-page/thumbnail-selection.svelte b/web/src/lib/components/album-page/thumbnail-selection.svelte index 9f964c4ada537..9e6c786d2230a 100644 --- a/web/src/lib/components/album-page/thumbnail-selection.svelte +++ b/web/src/lib/components/album-page/thumbnail-selection.svelte @@ -37,7 +37,7 @@ disabled={selectedThumbnail == undefined} on:click={() => dispatch('thumbnail', selectedThumbnail)} > - Done + {$t('done')} diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 1e8dd56d9cd1e..5521d521735b3 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -24,9 +24,9 @@ let selectedUsers: Record = {}; const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [ - { title: $t('editor'), value: AlbumUserRole.Editor, icon: mdiPencil }, - { title: $t('viewer'), value: AlbumUserRole.Viewer, icon: mdiEye }, - { title: $t('remove'), value: 'none' }, + { title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil }, + { title: $t('role_viewer'), value: AlbumUserRole.Viewer, icon: mdiEye }, + { title: $t('remove_user'), value: 'none' }, ]; const dispatch = createEventDispatcher<{ @@ -110,7 +110,7 @@ {#if users.length + Object.keys(selectedUsers).length === 0}

- Looks like you have shared this album with all users or you don't have any user to share with. + {$t('album_share_no_users')}

{/if} diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index a4cc7cf89d310..4664dcc3c5ad9 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -8,6 +8,7 @@ import { isTenMinutesApart } from '$lib/utils/timesince'; import { ReactionType, + Type, createActivity, deleteActivity, getActivities, @@ -41,7 +42,7 @@ const diff = dateTime.diffNow().shiftTo(...units); const unit = units.find((unit) => diff.get(unit) !== 0) || 'second'; - const relativeFormatter = new Intl.RelativeTimeFormat('en', { + const relativeFormatter = new Intl.RelativeTimeFormat($locale, { numeric: 'auto', }); return relativeFormatter.format(Math.trunc(diff.as(unit)), unit); @@ -115,8 +116,13 @@ } else { dispatch('deleteComment'); } + + const deleteMessages: Record = { + [Type.Comment]: $t('comment_deleted'), + [Type.Like]: $t('like_deleted'), + }; notificationController.show({ - message: `${reaction.type} deleted`, + message: deleteMessages[reaction.type], type: NotificationType.Info, }); } catch (error) { @@ -216,7 +222,12 @@
- {`${reaction.user.name} liked ${assetType ? `this ${getAssetType(assetType).toLowerCase()}` : 'it'}`} + {$t('user_liked', { + values: { + user: reaction.user.name, + type: assetType ? getAssetType(assetType).toLowerCase() : null, + }, + })}
{#if assetId === undefined && reaction.assetId} import type { AlbumResponseDto } from '@immich/sdk'; + import { t } from 'svelte-i18n'; export let album: AlbumResponseDto; -{album.assetCount} items +{$t('items_count', { values: { count: album.assetCount } })} {#if album.shared} - • Shared + • {$t('shared')} {/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index d891051d78e28..fc1239d396153 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -225,18 +225,18 @@ onJobClick(AssetJobName.RefreshMetadata)} - text={getAssetJobName(AssetJobName.RefreshMetadata)} + text={$getAssetJobName(AssetJobName.RefreshMetadata)} /> onJobClick(AssetJobName.RegenerateThumbnail)} - text={getAssetJobName(AssetJobName.RegenerateThumbnail)} + text={$getAssetJobName(AssetJobName.RegenerateThumbnail)} /> {#if asset.type === AssetTypeEnum.Video} onJobClick(AssetJobName.TranscodeVideo)} - text={getAssetJobName(AssetJobName.TranscodeVideo)} + text={$getAssetJobName(AssetJobName.TranscodeVideo)} /> {/if} {/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 0225a92df26aa..e3cafae14c519 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -162,7 +162,7 @@ reactions = [...reactions, isLiked]; } } catch (error) { - handleError(error, "Can't change favorite for asset"); + handleError(error, $t('errors.unable_to_change_favorite')); } } }; @@ -189,7 +189,7 @@ const { comments } = await getActivityStatistics({ assetId: asset.id, albumId: album.id }); numberOfComments = comments; } catch (error) { - handleError(error, "Can't get number of comments"); + handleError(error, $t('errors.unable_to_get_comments_number')); } } }; @@ -395,10 +395,10 @@ notificationController.show({ type: NotificationType.Info, - message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`, + message: asset.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'), }); } catch (error) { - handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`); + handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } })); } }; @@ -429,7 +429,7 @@ notificationController.show({ type: NotificationType.Info, - message: `Restored asset`, + message: $t('restored_asset'), }); } catch (error) { handleError(error, $t('errors.unable_to_restore_assets')); @@ -446,9 +446,9 @@ const handleRunJob = async (name: AssetJobName) => { try { await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } }); - notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) }); + notificationController.show({ type: NotificationType.Info, message: $getAssetJobMessage(name) }); } catch (error) { - handleError(error, `Unable to submit job`); + handleError(error, $t('errors.unable_to_submit_job')); } }; @@ -528,7 +528,7 @@ timeout: 1500, }); } catch (error) { - handleError(error, 'Unable to update album cover'); + handleError(error, $t('errors.unable_to_update_album_cover')); } }; diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 5414871adbce7..ac3a4b4c9e2df 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -153,8 +153,7 @@
{$t('asset_offline')}

- This asset is offline. Immich can not access its file location. Please ensure the asset is available and - then rescan the library. + {$t('asset_offline_description')}

@@ -170,8 +169,8 @@
{#if unassignedFaces.length > 0} {#if ageInMonths <= 11} - Age {ageInMonths} months + {$t('age_months', { values: { months: ageInMonths } })} {:else if ageInMonths > 12 && ageInMonths <= 23} - Age 1 year, {ageInMonths - 12} months + {$t('age_year_months', { values: { months: ageInMonths - 12 } })} {:else} - Age {age} + {$t('age_years', { values: { years: age } })} {/if}

{/if} @@ -452,7 +451,7 @@ target="_blank" class="font-medium text-immich-primary" > - Open in OpenStreetMap + {$t('open_in_openstreetmap')}
diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 6166c38caab21..2ac78c990d47c 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -82,7 +82,7 @@ const mergedPerson = await getPerson({ id: person.id }); const count = results.filter(({ success }) => success).length; notificationController.show({ - message: `Merged ${count} ${count === 1 ? 'person' : 'people'}`, + message: $t('merged_people_count', { values: { count: count } }), type: NotificationType.Info, }); dispatch('merge', mergedPerson); @@ -101,7 +101,7 @@ {#if hasSelection} - {$t('selected')} {selectedPeople.length} + {$t('selected_count', { values: { count: selectedPeople.length } })} {:else} {$t('merge_people')} {/if} diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte index 9621f6862fd01..3f8a8b36d2709 100644 --- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte +++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte @@ -99,10 +99,10 @@
-

Are these the same person?

+

{$t('are_these_the_same_person')}

-

They will be merged together

+

{$t('they_will_be_merged_together')}

diff --git a/web/src/lib/components/faces-page/people-search.svelte b/web/src/lib/components/faces-page/people-search.svelte index ed33ded46bd36..cfd4c8f29a2f5 100644 --- a/web/src/lib/components/faces-page/people-search.svelte +++ b/web/src/lib/components/faces-page/people-search.svelte @@ -62,7 +62,7 @@ searchedPeople = data; searchWord = searchName; } catch (error) { - handleError(error, $t('cant_search_people')); + handleError(error, $t('errors.cant_search_people')); } finally { clearTimeout(timeout); timeout = null; diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index a25e30310350e..a848a17e75113 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -68,7 +68,7 @@ allPeople = people; peopleWithFaces = await getFaces({ id: assetId }); } catch (error) { - handleError(error, $t('cant_get_faces')); + handleError(error, $t('errors.cant_get_faces')); } finally { clearTimeout(timeout); } @@ -142,11 +142,11 @@ } notificationController.show({ - message: `Edited ${numberOfChanges} ${numberOfChanges > 1 ? 'people' : 'person'}`, + message: $t('people_edits_count', { values: { count: numberOfChanges } }), type: NotificationType.Info, }); } catch (error) { - handleError(error, $t('cant_apply_changes')); + handleError(error, $t('errors.cant_apply_changes')); } } @@ -194,7 +194,7 @@ class="justify-self-end rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50" on:click={() => handleEditFaces()} > - Done + {$t('done')} {:else} @@ -299,7 +299,7 @@

- Date of birth is used to calculate the age of this person at the time of a photo. + {$t('birthdate_set_description')}

diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte index 1e480f85dbaff..c89c8338d3791 100644 --- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte +++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte @@ -19,7 +19,7 @@ import { NotificationType, notificationController } from '../shared-components/notification/notification'; import FaceThumbnail from './face-thumbnail.svelte'; import PeopleList from './people-list.svelte'; - import { s } from '$lib/utils'; + import { t } from 'svelte-i18n'; export let assetIds: string[]; export let personAssets: PersonResponseDto; @@ -77,11 +77,11 @@ await reassignFaces({ id: data.id, assetFaceUpdateDto: { data: selectedPeople } }); notificationController.show({ - message: `Re-assigned ${assetIds.length} asset${s(assetIds.length)} to a new person`, + message: $t('reassigned_assets_to_new_person', { values: { count: assetIds.length } }), type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Unable to reassign assets to a new person'); + handleError(error, $t('errors.unable_to_reassign_assets_new_person')); } finally { clearTimeout(timeout); } @@ -97,14 +97,17 @@ if (selectedPerson) { await reassignFaces({ id: selectedPerson.id, assetFaceUpdateDto: { data: selectedPeople } }); notificationController.show({ - message: `Re-assigned ${assetIds.length} asset${s(assetIds.length)} to ${ - selectedPerson.name || 'an existing person' - }`, + message: $t('reassigned_assets_to_existing_person', { + values: { count: assetIds.length, name: selectedPerson.name || null }, + }), type: NotificationType.Info, }); } } catch (error) { - handleError(error, `Unable to reassign assets to ${selectedPerson?.name || 'an existing person'}`); + handleError( + error, + $t('errors.unable_to_reassign_assets_existing_person', { values: { name: selectedPerson?.name || null } }), + ); } finally { clearTimeout(timeout); } @@ -128,7 +131,7 @@
{$t('create_new_person')} {$t('reassign')}
diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte index 54667cd300438..c66b09040fb77 100644 --- a/web/src/lib/components/forms/admin-registration-form.svelte +++ b/web/src/lib/components/forms/admin-registration-form.svelte @@ -33,7 +33,7 @@ await signUpAdmin({ signUpDto: { email, password, name } }); await goto(AppRoute.AUTH_LOGIN); } catch (error) { - handleError(error, 'errors.unable_to_create_admin_account'); + handleError(error, $t('errors.unable_to_create_admin_account')); errorMessage = $t('errors.unable_to_create_admin_account'); } } diff --git a/web/src/lib/components/forms/api-key-form.svelte b/web/src/lib/components/forms/api-key-form.svelte index 9273faf38e245..55ec258b40f30 100644 --- a/web/src/lib/components/forms/api-key-form.svelte +++ b/web/src/lib/components/forms/api-key-form.svelte @@ -22,7 +22,7 @@ dispatch('submit', apiKey); } else { notificationController.show({ - message: "Your API Key name shouldn't be empty", + message: $t('api_key_empty'), type: NotificationType.Warning, }); } diff --git a/web/src/lib/components/forms/api-key-secret.svelte b/web/src/lib/components/forms/api-key-secret.svelte index 1722029ae791b..b7bf8e1836270 100644 --- a/web/src/lib/components/forms/api-key-secret.svelte +++ b/web/src/lib/components/forms/api-key-secret.svelte @@ -17,7 +17,7 @@ handleDone()}>

- This value will only be shown once. Please be sure to copy it before closing the window. + {$t('api_key_description')}

diff --git a/web/src/lib/components/forms/change-password-form.svelte b/web/src/lib/components/forms/change-password-form.svelte index add87bb0ab359..799dde7ef3787 100644 --- a/web/src/lib/components/forms/change-password-form.svelte +++ b/web/src/lib/components/forms/change-password-form.svelte @@ -57,6 +57,6 @@

{success}

{/if}
- +
diff --git a/web/src/lib/components/forms/edit-album-form.svelte b/web/src/lib/components/forms/edit-album-form.svelte index 0c3b8e1044e94..bcb097eca671e 100644 --- a/web/src/lib/components/forms/edit-album-form.svelte +++ b/web/src/lib/components/forms/edit-album-form.svelte @@ -30,7 +30,7 @@ album.description = description; onEditSuccess?.(album); } catch (error) { - handleError(error, 'Unable to update album info'); + handleError(error, $t('errors.unable_to_update_album_info')); } finally { isSubmitting = false; } diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index 9bfffa90bbf09..828927a13abab 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -36,7 +36,7 @@ return; } catch (error) { console.error('Error [login-form] [oauth.callback]', error); - oauthError = getServerErrorMessage(error) || 'Unable to complete OAuth login'; + oauthError = getServerErrorMessage(error) || $t('errors.unable_to_complete_oauth_login'); oauthLoading = false; } } @@ -48,7 +48,7 @@ return; } } catch (error) { - handleError(error, 'Unable to connect!'); + handleError(error, $t('errors.unable_to_connect')); } oauthLoading = false; @@ -74,7 +74,7 @@ await onSuccess(); return; } catch (error) { - errorMessage = getServerErrorMessage(error) || 'Incorrect email or password'; + errorMessage = getServerErrorMessage(error) || $t('errors.incorrect_email_or_password'); loading = false; return; } @@ -86,7 +86,7 @@ const success = await oauth.authorize(window.location); if (!success) { oauthLoading = false; - oauthError = 'Unable to login with OAuth'; + oauthError = $t('errors.unable_to_login_with_oauth'); } }; @@ -124,7 +124,7 @@ {:else} - Login + {$t('to_login')} {/if} @@ -138,7 +138,7 @@ - or + {$t('or')} {/if} diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte index ccbdc9b03c1fd..b442396c84152 100644 --- a/web/src/lib/components/map-page/map-settings-modal.svelte +++ b/web/src/lib/components/map-page/map-settings-modal.svelte @@ -57,7 +57,7 @@ settings.dateBefore = ''; }} > - Remove custom date range + {$t('remove_custom_date_range')} @@ -70,7 +70,7 @@ options={[ { value: '', - text: 'All', + text: $t('all'), }, { value: Duration.fromObject({ hours: 24 }).toISO() || '', @@ -101,7 +101,7 @@ settings.relativeDate = ''; }} > - Use custom date range instead + {$t('use_custom_date_range')} diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index a3c7a0b93dbb7..c2d318ccdabfe 100644 --- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte @@ -16,9 +16,9 @@

- Welcome, {$user.name} + {$t('onboarding_welcome_user', { values: { user: $user.name } })}

-

Let's get your instance set up with some common settings.

+

{$t('onboarding_welcome_description')}

diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index 2f2928ab4d4cc..ff15b8b64ad8d 100644 --- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte @@ -19,7 +19,7 @@

{$t('color_theme').toUpperCase()}

-

Choose a color theme for your instance. You can change this later in your settings.

+

{$t('onboarding_theme_description')}

diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte index 4460548c387a8..ca61d54d43689 100644 --- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte +++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte @@ -24,7 +24,7 @@ try { const ids = [...getOwnedAssets()].map(({ id }) => id); await runAssetJobs({ assetJobsDto: { assetIds: ids, name } }); - notificationController.show({ message: getAssetJobMessage(name), type: NotificationType.Info }); + notificationController.show({ message: $getAssetJobMessage(name), type: NotificationType.Info }); clearSelect(); } catch (error) { handleError(error, $t('errors.unable_to_submit_job')); @@ -34,6 +34,6 @@ {#each jobs as job} {#if isAllVideos || job !== AssetJobName.TranscodeVideo} - handleRunJob(job)} /> + handleRunJob(job)} /> {/if} {/each} diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte index 503f0ba156566..1d723b1a9dc8c 100644 --- a/web/src/lib/components/photos-page/actions/favorite-action.svelte +++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte @@ -44,13 +44,15 @@ onFavorite(ids, isFavorite); notificationController.show({ - message: isFavorite ? `Added ${ids.length} to favorites` : `Removed ${ids.length} from favorites`, + message: isFavorite + ? $t('added_to_favorites_count', { values: { count: ids.length } }) + : $t('removed_from_favorites_count', { values: { count: ids.length } }), type: NotificationType.Info, }); clearSelect(); } catch (error) { - handleError(error, `Unable to ${isFavorite ? 'add to' : 'remove from'} favorites`); + handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: isFavorite } })); } finally { loading = false; } diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte index 90bdb39b716aa..251706a8c5b85 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte @@ -8,7 +8,6 @@ import { mdiDeleteOutline, mdiImageRemoveOutline } from '@mdi/js'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; - import { s } from '$lib/utils'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; @@ -21,7 +20,7 @@ const removeFromAlbum = async () => { const isConfirmed = await dialogController.show({ id: 'remove-from-album', - prompt: `Are you sure you want to remove ${getAssets().size} asset${s(getAssets().size)} from the album?`, + prompt: $t('remove_assets_album_confirmation', { values: { count: getAssets().size } }), }); if (!isConfirmed) { @@ -42,7 +41,7 @@ const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, - message: `Removed ${count} asset${s(count)}`, + message: $t('assets_removed_count', { values: { count: count } }), }); clearSelect(); @@ -50,7 +49,7 @@ console.error('Error [album-viewer] [removeAssetFromAlbum]', error); notificationController.show({ type: NotificationType.Error, - message: 'Error removing assets from album, check console for more details', + message: $t('errors.error_removing_assets_from_album'), }); } }; diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index ea8c3e3bec2ec..f687792bd57fe 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -1,6 +1,6 @@ diff --git a/web/src/lib/components/photos-page/actions/restore-assets.svelte b/web/src/lib/components/photos-page/actions/restore-assets.svelte index 36f8d214f49be..19e1c206fdba9 100644 --- a/web/src/lib/components/photos-page/actions/restore-assets.svelte +++ b/web/src/lib/components/photos-page/actions/restore-assets.svelte @@ -27,7 +27,7 @@ onRestore?.(ids); notificationController.show({ - message: `Restored ${ids.length}`, + message: $t('assets_restored_count', { values: { count: ids.length } }), type: NotificationType.Info, }); diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index 1a2af7b955de4..060aa89f1909e 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -14,7 +14,6 @@ dispatch('cancel')} @@ -38,10 +37,10 @@ this asset? This will also remove it from its album(s). {/if}

-

You cannot undo this action!

+

{$t('cannot_undo_this_action')}

- +
diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 831c98db1d27a..af5c54c9880cd 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -57,11 +57,11 @@ const added = data.filter((item) => item.success).length; notificationController.show({ - message: `Added ${added} assets`, + message: $t('assets_added_count', { values: { count: added } }), type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Unable to add assets to shared link'); + handleError(error, $t('errors.unable_to_add_assets_to_shared_link')); } }; diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte index 85d68b7264d19..fb6e6788bcdd7 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -99,17 +99,16 @@ {#if !shared}

- {#if search.length === 0}ALL - {/if}ALBUMS + {(search.length === 0 ? $t('all_albums') : $t('albums')).toUpperCase()}

{/if} {#each filteredAlbums as album (album.id)} handleSelect(album)} /> {/each} {:else if albums.length > 0} -

It looks like you do not have any albums with this name yet.

+

{$t('no_albums_with_name_yet')}

{:else} -

It looks like you do not have any albums yet.

+

{$t('no_albums_yet')}

{/if}
{/if} diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index dfff248816157..9feebe91ffddb 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -89,7 +89,7 @@ // skip error when a newer search is happening if (latestSearchTimeout === searchTimeout) { places = []; - handleError(error, $t('cant_search_places')); + handleError(error, $t('errors.cant_search_places')); showLoadingSpinner = false; } }); diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 7e6e3f54232b0..c212849a5b011 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -228,7 +228,7 @@ id={`${listboxId}-${0}`} on:click={() => closeDropdown()} > - No results + {$t('no_results')} {/if} {#each filteredOptions as option, index (option.label)} diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 512b415a6b57f..97c3aaf17e62c 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -102,7 +102,7 @@ sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key); dispatch('created'); } catch (error) { - handleError(error, 'Failed to create shared link'); + handleError(error, $t('errors.failed_to_create_shared_link')); } }; @@ -134,7 +134,7 @@ onClose(); } catch (error) { - handleError(error, 'Failed to edit shared link'); + handleError(error, $t('errors.failed_to_edit_shared_link')); } }; @@ -150,19 +150,18 @@
{#if shareType === SharedLinkType.Album} {#if !editingLink} -
Let anyone with the link see photos and people in this album.
+
{$t('album_with_link_access')}
{:else}
- Public album | {editingLink.album?.albumName} + {$t('public_album')} | + {editingLink.album?.albumName}
{/if} {/if} {#if shareType === SharedLinkType.Individual} {#if !editingLink} -
Let anyone with the link see the selected photo(s)
+
{$t('create_link_to_share_description')}
{:else}
{$t('individual_share')} | @@ -204,13 +203,13 @@
- +
diff --git a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte index 6acd819533be5..50d5fe56ce31f 100644 --- a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte +++ b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte @@ -5,7 +5,7 @@ import { t } from 'svelte-i18n'; export let title = $t('confirm'); - export let prompt = 'Are you sure you want to do this?'; + export let prompt = $t('are_you_sure_to_do_this'); export let confirmText = $t('confirm'); export let confirmColor: Color = 'red'; export let cancelText = $t('cancel'); diff --git a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte index dc115c45b45e0..466b3d083e48a 100644 --- a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte +++ b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte @@ -5,6 +5,7 @@ import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { fileUploadHandler } from '$lib/utils/file-uploader'; import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation'; + import { t } from 'svelte-i18n'; $: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined; $: isShare = isSharedLinkRoute($page.route?.id); @@ -64,6 +65,6 @@ }} > -
Drop files anywhere to upload
+
{$t('drop_files_to_upload')}
{/if} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 647b695c7b3fd..819105e197cbc 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -13,6 +13,7 @@ import { navigate } from '$lib/utils/navigation'; import { AppRoute, AssetAction } from '$lib/constants'; import { goto } from '$app/navigation'; + import { t } from 'svelte-i18n'; const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>(); @@ -52,7 +53,7 @@ await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); } } catch (error) { - handleError(error, 'Cannot navigate to the next asset'); + handleError(error, $t('errors.cannot_navigate_next_asset')); } }; @@ -63,7 +64,7 @@ await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); } } catch (error) { - handleError(error, 'Cannot navigate to previous asset'); + handleError(error, $t('errors.cannot_navigate_previous_asset')); } }; diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index c43424db079f3..91f81466317d3 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -187,7 +187,9 @@ src={getAssetThumbnailUrl(feature.properties?.id)} class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary" alt={feature.properties?.city && feature.properties.country - ? `Map marker for images taken in ${feature.properties.city}, ${feature.properties.country}` + ? $t('map_marker_for_images', { + values: { city: feature.properties.city, country: feature.properties.country }, + }) : $t('map_marker_with_image')} /> {/if} diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts b/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts index 76b9c39564f6c..187b1e0ccb6bc 100644 --- a/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts +++ b/web/src/lib/components/shared-components/notification/__tests__/notification-card.spec.ts @@ -34,7 +34,7 @@ describe('NotificationCard component', () => { }, }); - expect(sut.getByTestId('title')).toHaveTextContent('Info'); + expect(sut.getByTestId('title')).toHaveTextContent('info'); expect(sut.getByTestId('message')).toHaveTextContent('Notification message'); }); }); diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte index 6ddeb7433ed23..0919bca035c33 100644 --- a/web/src/lib/components/shared-components/notification/notification-card.svelte +++ b/web/src/lib/components/shared-components/notification/notification-card.svelte @@ -77,7 +77,9 @@

- {notification.type.toString()} + {#if notification.type == NotificationType.Error}{$t('error')} + {:else if notification.type == NotificationType.Warning}{$t('warning')} + {:else if notification.type == NotificationType.Info}{$t('info')}{/if}

- +
diff --git a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte index f652f70fd899c..0ede8d7fa172c 100644 --- a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte @@ -29,7 +29,7 @@ const res = await getAllPeople({ withHidden: false }); return orderBySelectedPeopleFirst(res.people); } catch (error) { - handleError(error, $t('failed_to_get_people')); + handleError(error, $t('errors.failed_to_get_people')); } } @@ -93,10 +93,10 @@ > {#if showAllPeople} - Collapse + {$t('collapse')} {:else} - See all people + {$t('see_all_people')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte b/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte index 07cd8e1b0ec9c..68c5b2628c96f 100644 --- a/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte +++ b/web/src/lib/components/shared-components/settings/setting-buttons-row.svelte @@ -21,7 +21,7 @@ on:click={() => dispatch('reset', { default: true })} class="bg-none text-sm font-medium text-immich-primary hover:text-immich-primary/75 dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75" > - Reset to default + {$t('reset_to_default')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte index 4e33efd571312..3def0ce08d0f4 100644 --- a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte +++ b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte @@ -2,6 +2,7 @@ import Checkbox from '$lib/components/elements/checkbox.svelte'; import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; + import { t } from 'svelte-i18n'; export let value: string[]; export let options: { value: string; text: string }[]; @@ -27,7 +28,7 @@ transition:fly={{ x: 10, duration: 200, easing: quintOut }} class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" > - Unsaved change + {$t('unsaved_change')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-combobox.svelte b/web/src/lib/components/shared-components/settings/setting-combobox.svelte index f6c1aa81392c8..502cd94cce04f 100644 --- a/web/src/lib/components/shared-components/settings/setting-combobox.svelte +++ b/web/src/lib/components/shared-components/settings/setting-combobox.svelte @@ -2,6 +2,7 @@ import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; + import { t } from 'svelte-i18n'; export let title: string; export let comboboxPlaceholder: string; @@ -23,7 +24,7 @@ transition:fly={{ x: 10, duration: 200, easing: quintOut }} class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" > - Unsaved change + {$t('unsaved_change')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-dropdown.svelte b/web/src/lib/components/shared-components/settings/setting-dropdown.svelte index c3168dbb1c97d..5243a14931950 100644 --- a/web/src/lib/components/shared-components/settings/setting-dropdown.svelte +++ b/web/src/lib/components/shared-components/settings/setting-dropdown.svelte @@ -2,6 +2,7 @@ import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; import Dropdown, { type RenderedOption } from '$lib/components/elements/dropdown.svelte'; + import { t } from 'svelte-i18n'; export let title: string; export let subtitle = ''; @@ -23,7 +24,7 @@ transition:fly={{ x: 10, duration: 200, easing: quintOut }} class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" > - Unsaved change + {$t('unsaved_change')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte index d3da95ebe147e..04bc72f3f6b32 100644 --- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte +++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte @@ -12,6 +12,7 @@ import type { FormEventHandler } from 'svelte/elements'; import { fly } from 'svelte/transition'; import PasswordField from '../password-field.svelte'; + import { t } from 'svelte-i18n'; export let inputType: SettingInputFieldType; export let value: string | number; @@ -54,7 +55,7 @@ transition:fly={{ x: 10, duration: 200, easing: quintOut }} class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" > - Unsaved change + {$t('unsaved_change')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-select.svelte b/web/src/lib/components/shared-components/settings/setting-select.svelte index 99aa364455a22..b4efd90056386 100644 --- a/web/src/lib/components/shared-components/settings/setting-select.svelte +++ b/web/src/lib/components/shared-components/settings/setting-select.svelte @@ -2,6 +2,7 @@ import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; import { createEventDispatcher } from 'svelte'; + import { t } from 'svelte-i18n'; export let value: string | number; export let options: { value: string | number; text: string }[]; @@ -34,7 +35,7 @@ transition:fly={{ x: 10, duration: 200, easing: quintOut }} class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" > - Unsaved change + {$t('unsaved_change')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-switch.svelte b/web/src/lib/components/shared-components/settings/setting-switch.svelte index b2453fda276be..d933b27ab54fb 100644 --- a/web/src/lib/components/shared-components/settings/setting-switch.svelte +++ b/web/src/lib/components/shared-components/settings/setting-switch.svelte @@ -4,6 +4,7 @@ import { createEventDispatcher } from 'svelte'; import Slider from '$lib/components/elements/slider.svelte'; import { generateId } from '$lib/utils/generate-id'; + import { t } from 'svelte-i18n'; export let title: string; export let subtitle = ''; @@ -31,7 +32,7 @@ transition:fly={{ x: 10, duration: 200, easing: quintOut }} class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" > - Unsaved change + {$t('unsaved_change')} {/if} diff --git a/web/src/lib/components/shared-components/settings/setting-textarea.svelte b/web/src/lib/components/shared-components/settings/setting-textarea.svelte index 038c7e81a1782..2f579e9db4acb 100644 --- a/web/src/lib/components/shared-components/settings/setting-textarea.svelte +++ b/web/src/lib/components/shared-components/settings/setting-textarea.svelte @@ -1,6 +1,7 @@ {#if showModal} - (showModal = false)}> + (showModal = false)}>
{#if tag === 'link'} @@ -53,9 +53,9 @@
Your friend, Alex
- Server Version: {serverVersion} + {$t('server_version')}: {serverVersion}
- Latest Version: {releaseVersion} + {$t('latest_version')}: {releaseVersion}
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 3d75df4259df4..06dad3913aac2 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -30,13 +30,13 @@ expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject(); if (expirationCountdown.days && expirationCountdown.days > 0) { - return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' }); + return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'days' }); } else if (expirationCountdown.hours && expirationCountdown.hours > 0) { - return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'hours' }); + return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'hours' }); } else if (expirationCountdown.minutes && expirationCountdown.minutes > 0) { - return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'minutes' }); + return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'minutes' }); } else if (expirationCountdown.seconds && expirationCountdown.seconds > 0) { - return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'seconds' }); + return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'seconds' }); } }; @@ -63,11 +63,11 @@

{$t('expired')}

{:else}

- Expires {getCountDownExpirationDate()} + {$t('expires_date', { values: { date: getCountDownExpirationDate() } })}

{/if} {:else} -

Expires ∞

+

{$t('expires_date', { values: { date: '∞' } })}

{/if}
@@ -97,7 +97,7 @@
- Upload + {$t('upload')}
{/if} @@ -105,7 +105,7 @@
- Download + {$t('download')}
{/if} @@ -113,7 +113,7 @@
- EXIF + {$t('exif').toUpperCase()}
{/if} @@ -121,7 +121,7 @@
- Password + {$t('password')}
{/if} diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index e6823aeed3289..8e511abf4a847 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -17,7 +17,7 @@ const handleDelete = async (device: SessionResponseDto) => { const isConfirmed = await dialogController.show({ id: 'log-out-device', - prompt: 'Are you sure you want to log out this device?', + prompt: $t('logout_this_device_confirmation'), }); if (!isConfirmed) { @@ -26,9 +26,9 @@ try { await deleteSession({ id: device.id }); - notificationController.show({ message: `Logged out device`, type: NotificationType.Info }); + notificationController.show({ message: $t('logged_out_device'), type: NotificationType.Info }); } catch (error) { - handleError(error, 'Unable to log out device'); + handleError(error, $t('errors.unable_to_log_out_device')); } finally { await refresh(); } @@ -37,7 +37,7 @@ const handleDeleteAll = async () => { const isConfirmed = await dialogController.show({ id: 'log-out-all-devices', - prompt: 'Are you sure you want to log out all devices?', + prompt: $t('logout_all_device_confirmation'), }); if (!isConfirmed) { @@ -47,11 +47,11 @@ try { await deleteAllSessions(); notificationController.show({ - message: `Logged out all devices`, + message: $t('logged_out_all_devices'), type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Unable to log out all devices'); + handleError(error, $t('errors.unable_to_log_out_all_devices')); } finally { await refresh(); } diff --git a/web/src/lib/components/user-settings-page/notifications-settings.svelte b/web/src/lib/components/user-settings-page/notifications-settings.svelte index ee6026c52b1e5..275f628f0a552 100644 --- a/web/src/lib/components/user-settings-page/notifications-settings.svelte +++ b/web/src/lib/components/user-settings-page/notifications-settings.svelte @@ -32,9 +32,9 @@ $preferences.emailNotifications.albumInvite = data.emailNotifications.albumInvite; $preferences.emailNotifications.albumUpdate = data.emailNotifications.albumUpdate; - notificationController.show({ message: 'Saved settings', type: NotificationType.Info }); + notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info }); } catch (error) { - handleError(error, 'Unable to update settings'); + handleError(error, $t('errors.unable_to_update_settings')); } }; diff --git a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte index 40e517f41076c..88cda3cc0c698 100644 --- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte +++ b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte @@ -63,7 +63,7 @@ {/each} {:else}

- Looks like you shared your photos with all users or you don't have any user to share with. + {$t('photo_shared_all_users')}

{/if} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index cb8af2bfb98eb..6deaaea342e53 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -6,6 +6,7 @@ "actions": "Actions", "active": "Active", "activity": "Activity", + "activity_changed": "Activity is {enabled, select, true {enabled} other {disabled}}", "add": "Add", "add_a_description": "Add a description", "add_a_location": "Add a location", @@ -21,6 +22,9 @@ "add_to": "Add to...", "add_to_album": "Add to album", "add_to_shared_album": "Add to shared album", + "added_to_archive": "Added to archive", + "added_to_favorites": "Added to favorites", + "added_to_favorites_count": "Added {count} to favorites", "admin": { "add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".", "authentication_settings": "Authentication Settings", @@ -31,7 +35,7 @@ "cleared_jobs": "Cleared jobs for: {job}", "config_set_by_file": "Config is currently set by a config file", "confirm_delete_library": "Are you sure you want to delete {library} library?", - "confirm_delete_library_assets": "Are you sure you want to delete this library? This will delete all {count} contained assets from Immich and cannot be undone. Files will remain on disk.", + "confirm_delete_library_assets": "Are you sure you want to delete this library? This will delete {count, plural, one {# contained asset} other {all # contained assets}} from Immich and cannot be undone. Files will remain on disk.", "confirm_email_below": "To confirm, type \"{email}\" below", "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.", "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?", @@ -66,8 +70,8 @@ "job_settings": "Job Settings", "job_settings_description": "Manage job concurrency", "job_status": "Job Status", - "jobs_delayed": "{jobCount} delayed", - "jobs_failed": "{jobCount} failed", + "jobs_delayed": "{jobCount, plural, other {# delayed}}", + "jobs_failed": "{jobCount, plural, other {# failed}}", "library_created": "Created library: {library}", "library_cron_expression": "Cron expression", "library_cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. Crontab Guru", @@ -182,6 +186,8 @@ "paths_validated_successfully": "All paths validated successfully", "quota_size_gib": "Quota Size (GiB)", "refreshing_all_libraries": "Refreshing all libraries", + "registration": "Admin Registration", + "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", "removing_offline_files": "Removing Offline Files", "repair_all": "Repair All", "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", @@ -203,7 +209,7 @@ "slideshow_duration_description": "Number of seconds to display each image", "smart_search_job_description": "Run machine learning on assets to support smart search", "storage_template_enable_description": "Enable storage template engine", - "storage_template_hash_verification_enabled": "Hash verification failed", + "storage_template_hash_verification_enabled": "Hash verification enabled", "storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications", "storage_template_migration": "Storage template migration", "storage_template_migration_description": "Apply the current {template} to previously uploaded assets", @@ -308,21 +314,39 @@ "admin_password": "Admin Password", "administration": "Administration", "advanced": "Advanced", + "age_months": "Age {months, plural, one {# month} other {# months}}", + "age_year_months": "Age 1 year, {months, plural, one {# month} other {# months}}", + "age_years": "Age {years}", "album_added": "Album added", "album_added_notification_setting_description": "Receive an email notification when you are added to a shared album", "album_cover_updated": "Album cover updated", + "album_delete_confirmation": "Are you sure you want to delete the album {album}?\nIf this album is shared, other users will not be able to access it anymore.", "album_info_updated": "Album info updated", + "album_leave": "Leave album?", + "album_leave_confirmation": "Are you sure you want to leave {album}?", "album_name": "Album Name", "album_options": "Album options", + "album_remove_user": "Remove user?", + "album_remove_user_confirmation": "Are you sure you want to remove {user}?", + "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", + "album_user_left": "Left {album}", + "album_user_removed": "Removed {user}", + "album_with_link_access": "Let anyone with the link see photos and people in this album.", "albums": "Albums", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}", "all": "All", + "all_albums": "All albums", "all_people": "All people", + "all_videos": "All videos", "allow_dark_mode": "Allow dark mode", "allow_edits": "Allow edits", + "allow_public_user_to_download": "Allow public user to download", + "allow_public_user_to_upload": "Allow public user to upload", "api_key": "API Key", + "api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.", + "api_key_empty": "Your API Key name shouldn't be empty", "api_keys": "API Keys", "app_settings": "App Settings", "appears_in": "Appears in", @@ -330,34 +354,50 @@ "archive_or_unarchive_photo": "Archive or unarchive photo", "archive_size": "Archive Size", "archive_size_description": "Configure the archive size for downloads (in GiB)", - "archived": "Archived", + "archived_count": "{count, plural, other {Archived #}}", + "are_these_the_same_person": "Are these the same person?", + "are_you_sure_to_do_this": "Are you sure you want to do this?", + "asset_filename_is_offline": "Asset {filename} is offline", + "asset_has_unassigned_faces": "Asset has unassigned faces", "asset_offline": "Asset offline", + "asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.", "assets": "Assets", - "assets_moved_to_trash": "Moved {count, plural, one {# asset} other {# assets}} to trash", + "assets_added_count": "Added {count, plural, one {# asset} other {# assets}}", + "assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album", + "assets_added_to_name_count": "Added {count, plural, one {# asset} other {# assets} to {name}", + "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash", + "assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}", + "assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}", + "assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action!", + "assets_restored_count": "Restored {count, plural, one {# asset} other {# assets}}", + "assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}", + "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album", "authorized_devices": "Authorized Devices", "back": "Back", + "back_close_deselect": "Back, close, or deselect", "backward": "Backward", + "birthdate_saved": "Date of birth saved successfully", + "birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.", "blurred_background": "Blurred background", - "bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count} duplicate assets? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!", - "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count} duplicate assets? This will resolve all duplicate groups without deleting anything.", - "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count} duplicate assets? This will keep the largest asset of each group and trash all other duplicates.", + "bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!", + "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", + "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", "camera": "Camera", "camera_brand": "Camera brand", "camera_model": "Camera model", "cancel": "Cancel", "cancel_search": "Cancel search", "cannot_merge_people": "Cannot merge people", + "cannot_undo_this_action": "You cannot undo this action!", "cannot_update_the_description": "Cannot update the description", - "cant_apply_changes": "Can't apply changes", - "cant_get_faces": "Can't get faces", - "cant_search_people": "Can't search people", - "cant_search_places": "Can't search places", "change_date": "Change date", "change_expiration_time": "Change expiration time", "change_location": "Change location", "change_name": "Change name", "change_name_successfully": "Change name successfully", - "change_password": "Change password", + "change_password": "Change Password", + "change_password_description": "This is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", "check_all": "Check All", @@ -369,9 +409,12 @@ "clear_message": "Clear message", "clear_value": "Clear value", "close": "Close", + "collapse": "Collapse", "collapse_all": "Collapse all", "color_theme": "Color theme", + "comment_deleted": "Comment deleted", "comment_options": "Comment options", + "comments_and_likes": "Comments & likes", "comments_are_disabled": "Comments are disabled", "confirm": "Confirm", "confirm_admin_password": "Confirm Admin Password", @@ -397,7 +440,9 @@ "create_library": "Create Library", "create_link": "Create link", "create_link_to_share": "Create link to share", + "create_link_to_share_description": "Let anyone with the link see the selected photo(s)", "create_new_person": "Create new person", + "create_new_person_hint": "Assign selected assets to a new person", "create_new_user": "Create new user", "create_user": "Create user", "created": "Created", @@ -435,14 +480,18 @@ "display_order": "Display order", "display_original_photos": "Display original photos", "display_original_photos_setting_description": "Prefer to display the original photo when viewing an asset rather than thumbnails when the original asset is web-compatible. This may result in slower photo display speeds.", + "do_not_show_again": "Do not show this message again", "done": "Done", "download": "Download", "download_settings": "Download", "download_settings_description": "Manage settings related to asset download", "downloading": "Downloading", + "downloading_asset_filename": "Downloading asset {filename}", + "drop_files_to_upload": "Drop files anywhere to upload", "duplicates": "Duplicates", "duplicates_description": "Resolve each group by indicating which, if any, are duplicates", "duration": "Duration", + "edit": "Edit", "edit_album": "Edit album", "edit_avatar": "Edit avatar", "edit_date": "Edit date", @@ -459,52 +508,99 @@ "edit_title": "Edit Title", "edit_user": "Edit user", "edited": "Edited", - "editor": "Editor", "email": "Email", "empty_trash": "Empty trash", + "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", + "enable": "Enable", + "enabled": "Enabled", "end_date": "End date", "error": "Error", "error_loading_image": "Error loading image", + "error_title": "Error - Something went wrong", "errors": { + "cannot_navigate_next_asset": "Cannot navigate to the next asset", + "cannot_navigate_previous_asset": "Cannot navigate to previous asset", + "cant_apply_changes": "Can't apply changes", + "cant_change_activity": "Can't {enabled, select, true {disable} other {enable}} activity", + "cant_change_asset_favorite": "Can't change favorite for asset", + "cant_change_metadata_assets_count": "Can't change metadata of {count, plural, one {# asset} other {# assets}}", + "cant_get_faces": "Can't get faces", + "cant_get_number_of_comments": "Can't get number of comments", + "cant_search_people": "Can't search people", + "cant_search_places": "Can't search places", + "cleared_jobs": "Cleared jobs for: {job}", + "error_adding_assets_to_album": "Error adding assets to album", + "error_adding_users_to_album": "Error adding users to album", + "error_deleting_shared_user": "Error deleting shared user", + "error_downloading": "Error downloading {filename}", + "error_removing_assets_from_album": "Error removing assets from album, check console for more details", + "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", + "failed_job_command": "Command {command} failed for job: {job}", + "failed_to_create_album": "Failed to create album", + "failed_to_create_shared_link": "Failed to create shared link", + "failed_to_edit_shared_link": "Failed to edit shared link", + "failed_to_get_people": "Failed to get people", + "failed_to_stack_assets": "Failed to stack assets", + "failed_to_unstack_assets": "Failed to un-stack assets", "import_path_already_exists": "This import path already exists.", + "incorrect_email_or_password": "Incorrect email or password", "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", + "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "quota_higher_than_disk_size": "You set a quota higher than the disk size", "repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}", "unable_to_add_album_users": "Unable to add users to album", + "unable_to_add_assets_to_shared_link": "Unable to add assets to shared link", "unable_to_add_comment": "Unable to add comment", "unable_to_add_exclusion_pattern": "Unable to add exclusion pattern", "unable_to_add_import_path": "Unable to add import path", "unable_to_add_partners": "Unable to add partners", + "unable_to_add_remove_archive": "Unable to {archived, select, true {remove asset from} other {add asset to}} archive", + "unable_to_add_remove_favorites": "Unable to {favorite, select, true {add asset to} other {remove asset from}} favorites", + "unable_to_archive_unarchive": "Unable to {archived, select, true {archive} other {unarchive}}", "unable_to_change_album_user_role": "Unable to change the album user's role", "unable_to_change_date": "Unable to change date", + "unable_to_change_favorite": "Unable to change favorite for asset", "unable_to_change_location": "Unable to change location", "unable_to_change_password": "Unable to change password", + "unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}", + "unable_to_complete_oauth_login": "Unable to complete OAuth login", + "unable_to_connect": "Unable to connect", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", + "unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_api_key": "Unable to create a new API Key", "unable_to_create_library": "Unable to create library", "unable_to_create_user": "Unable to create user", "unable_to_delete_album": "Unable to delete album", "unable_to_delete_asset": "Unable to delete asset", + "unable_to_delete_assets": "Error deleting assets", "unable_to_delete_exclusion_pattern": "Unable to delete exclusion pattern", "unable_to_delete_import_path": "Unable to delete import path", "unable_to_delete_shared_link": "Unable to delete shared link", "unable_to_delete_user": "Unable to delete user", + "unable_to_download_files": "Unable to download files", "unable_to_edit_exclusion_pattern": "Unable to edit exclusion pattern", "unable_to_edit_import_path": "Unable to edit import path", "unable_to_empty_trash": "Unable to empty trash", "unable_to_enter_fullscreen": "Unable to enter fullscreen", "unable_to_exit_fullscreen": "Unable to exit fullscreen", + "unable_to_get_comments_number": "Unable to get number of comments", "unable_to_hide_person": "Unable to hide person", "unable_to_link_oauth_account": "Unable to link OAuth account", "unable_to_load_album": "Unable to load album", "unable_to_load_asset_activity": "Unable to load asset activity", "unable_to_load_items": "Unable to load items", "unable_to_load_liked_status": "Unable to load liked status", + "unable_to_log_out_all_devices": "Unable to log out all devices", + "unable_to_log_out_device": "Unable to log out device", + "unable_to_login_with_oauth": "Unable to login with OAuth", "unable_to_play_video": "Unable to play video", + "unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}", + "unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person", "unable_to_refresh_user": "Unable to refresh user", "unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_api_key": "Unable to remove API Key", + "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link", "unable_to_remove_library": "Unable to remove library", "unable_to_remove_offline_files": "Unable to remove offline files", "unable_to_remove_partner": "Unable to remove partner", @@ -526,23 +622,26 @@ "unable_to_submit_job": "Unable to submit job", "unable_to_trash_asset": "Unable to trash asset", "unable_to_unlink_account": "Unable to unlink account", + "unable_to_update_album_cover": "Unable to update album cover", + "unable_to_update_album_info": "Unable to update album info", "unable_to_update_library": "Unable to update library", "unable_to_update_location": "Unable to update location", "unable_to_update_settings": "Unable to update settings", "unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_user": "Unable to update user" }, + "exif": "Exif", "exit_slideshow": "Exit Slideshow", "expand_all": "Expand all", "expire_after": "Expire after", "expired": "Expired", + "expires_date": "Expires {date}", "explore": "Explore", "export": "Export", "export_as_json": "Export as JSON", "extension": "Extension", "external": "External", "external_libraries": "External Libraries", - "failed_to_get_people": "Failed to get people", "favorite": "Favorite", "favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorites": "Favorites", @@ -563,7 +662,11 @@ "go_to_search": "Go to search", "go_to_share_page": "Go to share page", "group_albums_by": "Group albums by...", + "group_no": "No grouping", + "group_owner": "Group by owner", + "group_year": "Group by year", "has_quota": "Has quota", + "hi_user": "Hi {name} ({email})", "hide_gallery": "Hide gallery", "hide_password": "Hide password", "hide_person": "Hide person", @@ -589,6 +692,7 @@ }, "invite_people": "Invite People", "invite_to_album": "Invite to album", + "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Jobs", "keep": "Keep", "keep_all": "Keep All", @@ -596,12 +700,14 @@ "language": "Language", "language_setting_description": "Select your preferred language", "last_seen": "Last seen", + "latest_version": "Latest Version", "leave": "Leave", "let_others_respond": "Let others respond", "level": "Level", "library": "Library", "library_options": "Library options", "light": "Light", + "like_deleted": "Like deleted", "link_options": "Link options", "link_to_oauth": "Link to OAuth", "linked_oauth_account": "Linked OAuth account", @@ -610,7 +716,12 @@ "loading_search_results_failed": "Loading search results failed", "log_out": "Log out", "log_out_all_devices": "Log Out All Devices", + "logged_out_all_devices": "Logged out all devices", + "logged_out_device": "Logged out device", + "login": "Login", "login_has_been_disabled": "Login has been disabled.", + "logout_all_device_confirmation": "Are you sure you want to log out all devices?", + "logout_this_device_confirmation": "Are you sure you want to log out this device?", "look": "Look", "loop_videos": "Loop videos", "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", @@ -623,6 +734,7 @@ "manage_your_devices": "Manage your logged-in devices", "manage_your_oauth_connection": "Manage your OAuth connection", "map": "Map", + "map_marker_for_images": "Map marker for images taken in {city}, {country}", "map_marker_with_image": "Map marker with image", "map_settings": "Map settings", "matches": "Matches", @@ -636,6 +748,7 @@ "merge_people_limit": "You can only merge up to 5 faces at a time", "merge_people_prompt": "Do you want to merge these people? This action is irreversible.", "merge_people_successfully": "Merge people successfully", + "merged_people_count": "Merged {count, plural, one {# person} other {# people}}", "minimize": "Minimize", "minute": "Minute", "missing": "Missing", @@ -651,11 +764,14 @@ "new_password": "New password", "new_person": "New person", "new_user_created": "New user created", + "new_version_available": "NEW VERSION AVAILABLE", "newest_first": "Newest first", "next": "Next", "next_memory": "Next memory", "no": "No", "no_albums_message": "Create an album to organize your photos and videos", + "no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.", + "no_albums_yet": "It looks like you do not have any albums yet.", "no_archived_assets_message": "Archive photos and videos to hide them from your Photos view", "no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", "no_duplicates_found": "No duplicates were found.", @@ -666,6 +782,7 @@ "no_name": "No Name", "no_places": "No places", "no_results": "No results", + "no_results_description": "Try a synonym or more general keyword", "no_shared_albums_message": "Create an album to share photos and videos with people in your network", "not_in_any_album": "Not in any album", "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", @@ -680,12 +797,20 @@ "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "ok": "Ok", "oldest_first": "Oldest first", + "onboarding": "Onboarding", + "onboarding_storage_template_description": "When enabled, this feature will auto-organize files based on a user-defined template. Due to stability issues the feature has been turned off by default. For more information, please see the [documentation].", + "onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.", + "onboarding_welcome_description": "Let's get your instance set up with some common settings.", + "onboarding_welcome_user": "Welcome, {user}", "online": "Online", "only_favorites": "Only favorites", "only_refreshes_modified_files": "Only refreshes modified files", + "open_in_openstreetmap": "Open in OpenStreetMap", "open_the_search_filters": "Open the search filters", "options": "Options", + "or": "or", "organize_your_library": "Organize your library", + "original": "original", "other": "Other", "other_devices": "Other devices", "other_variables": "Other variables", @@ -702,9 +827,9 @@ "password_required": "Password Required", "password_reset_success": "Password reset success", "past_durations": { - "days": "Past {days, plural, one {day} other {{days, number} days}}", - "hours": "Past {hours, plural, one {hour} other {{hours, number} hours}}", - "years": "Past {years, plural, one {year} other {{years, number} years}}" + "days": "Past {days, plural, one {day} other {# days}}", + "hours": "Past {hours, plural, one {hour} other {# hours}}", + "years": "Past {years, plural, one {year} other {# years}}" }, "path": "Path", "pattern": "Pattern", @@ -713,14 +838,19 @@ "paused": "Paused", "pending": "Pending", "people": "People", + "people_edits_count": "Edited {count, plural, one {# person} other {# people}}", "people_sidebar_description": "Display a link to People in the sidebar", "permanent_deletion_warning": "Permanent deletion warning", "permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets", "permanently_delete": "Permanently delete", + "permanently_delete_assets_count": "Permanently delete {count, plural, one {asset} other {assets}}", "permanently_deleted_asset": "Permanently deleted asset", - "permanently_deleted_assets": "Permanently deleted {count, plural, one {# asset} other {# assets}}", + "permanently_deleted_assets_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}", "person": "Person", + "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "photo_shared_all_users": "Looks like you shared your photos with all users or you don't have any user to share with.", "photos": "Photos", + "photos_and_videos": "Photos & Videos", "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_from_previous_years": "Photos from previous years", "pick_a_location": "Pick a location", @@ -738,20 +868,39 @@ "previous_or_next_photo": "Previous or next photo", "primary": "Primary", "profile_picture_set": "Profile picture set.", + "public_album": "Public album", "public_share": "Public Share", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", + "reassign": "Reassign", + "reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}", + "reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person", + "reassing_hint": "Assign selected assets to an existing person", "recent": "Recent", "recent_searches": "Recent searches", "refresh": "Refresh", + "refresh_encoded_videos": "Refresh encoded videos", + "refresh_metadata": "Refresh metadata", + "refresh_thumbnails": "Refresh thumbnails", "refreshed": "Refreshed", "refreshes_every_file": "Refreshes every file", + "refreshing_encoded_video": "Refreshing encoded video", + "refreshing_metadata": "Refreshing metadata", + "regenerating_thumbnails": "Regenerating thumbnails", "remove": "Remove", + "remove_assets_album_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from the album?", + "remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?", + "remove_assets_title": "Remove assets?", + "remove_custom_date_range": "Remove custom date range", "remove_from_album": "Remove from album", "remove_from_favorites": "Remove from favorites", "remove_from_shared_link": "Remove from shared link", "remove_offline_files": "Remove Offline Files", + "remove_user": "Remove user", "removed_api_key": "Removed API Key: {name}", + "removed_from_archive": "Removed from archive", + "removed_from_favorites": "Removed from favorites", + "removed_from_favorites_count": "Removed {count} from favorites", "rename": "Rename", "repair": "Repair", "repair_no_results_message": "Untracked and missing files will show up here", @@ -761,14 +910,18 @@ "reset": "Reset", "reset_password": "Reset password", "reset_people_visibility": "Reset people visibility", + "reset_to_default": "Reset to default", "resolved_all_duplicates": "Resolved all duplicates", "restore": "Restore", "restore_all": "Restore all", "restore_user": "Restore user", + "restored_asset": "Restored asset", "resume": "Resume", "retry_upload": "Retry upload", "review_duplicates": "Review duplicates", "role": "Role", + "role_editor": "Editor", + "role_viewer": "Viewer", "save": "Save", "saved_api_key": "Saved API Key", "saved_profile": "Saved profile", @@ -787,6 +940,8 @@ "search_city": "Search city...", "search_country": "Search country...", "search_for_existing_person": "Search for existing person", + "search_no_people": "No people", + "search_no_people_named": "No people named \"{name}\"", "search_people": "Search people", "search_places": "Search places", "search_state": "Search state...", @@ -795,21 +950,25 @@ "search_your_photos": "Search your photos", "searching_locales": "Searching locales...", "second": "Second", + "see_all_people": "See all people", "select_album_cover": "Select album cover", "select_all": "Select all", "select_avatar_color": "Select avatar color", "select_face": "Select face", "select_featured_photo": "Select featured photo", + "select_from_computer": "Select from computer", "select_keep_all": "Select keep all", "select_library_owner": "Select library owner", "select_new_face": "Select new face", "select_photos": "Select photos", "select_trash_all": "Select trash all", "selected": "Selected", + "selected_count": "{count, plural, other {# selected}}", "send_message": "Send message", "send_welcome_email": "Send welcome email", "server": "Server", "server_stats": "Server Stats", + "server_version": "Server Version", "set": "Set", "set_as_album_cover": "Set as album cover", "set_as_profile_picture": "Set as profile picture", @@ -821,13 +980,15 @@ "share": "Share", "shared": "Shared", "shared_by": "Shared by", + "shared_by_user": "Shared by {user}", "shared_by_you": "Shared by you", "shared_from_partner": "Photos from {partner}", "shared_links": "Shared links", - "shared_photos_and_videos_count": "{assetCount} shared photos & videos.", + "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}", "shared_with_partner": "Shared with {partner}", "sharing": "Sharing", "sharing_sidebar_description": "Display a link to Sharing in the sidebar", + "shift_to_permanent_delete": "press ⇧ to permanently delete asset", "show_album_options": "Show album options", "show_and_hide_people": "Show & hide people", "show_file_location": "Show file location", @@ -850,8 +1011,15 @@ "slideshow": "Slideshow", "slideshow_settings": "Slideshow settings", "sort_albums_by": "Sort albums by...", + "sort_created": "Date created", + "sort_items": "Number of items", + "sort_modified": "Date modified", + "sort_oldest": "Oldest photo", + "sort_recent": "Most recent photo", + "sort_title": "Title", "stack": "Stack", "stack_selected_photos": "Stack selected photos", + "stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", "start": "Start", "start_date": "Start date", @@ -873,10 +1041,13 @@ "theme": "Theme", "theme_selection": "Theme selection", "theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference", + "they_will_be_merged_together": "They will be merged together", "time_based_memories": "Time-based memories", "timezone": "Timezone", "to_archive": "Archive", + "to_change_password": "Change password", "to_favorite": "Favorite", + "to_login": "Login", "to_trash": "Trash", "toggle_settings": "Toggle settings", "toggle_theme": "Toggle theme", @@ -885,11 +1056,12 @@ "trash": "Trash", "trash_all": "Trash All", "trash_count": "Trash {count}", + "trash_delete_asset": "Trash/Delete Asset", "trash_no_results_message": "Trashed photos and videos will show up here.", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", "type": "Type", "unarchive": "Unarchive", - "unarchived": "Unarchived", + "unarchived_count": "{count, plural, other {Unarchived #}}", "unfavorite": "Unfavorite", "unhide_person": "Unhide person", "unknown": "Unknown", @@ -899,18 +1071,30 @@ "unlinked_oauth_account": "Unlinked OAuth account", "unnamed_album": "Unnamed Album", "unnamed_share": "Unnamed Share", + "unsaved_change": "Unsaved change", "unselect_all": "Unselect all", "unstack": "Un-stack", + "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "untracked_files": "Untracked files", "untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "up_next": "Up next", "updated_password": "Updated password", "upload": "Upload", "upload_concurrency": "Upload concurrency", + "upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.", + "upload_progress": "Remaining {remaining} - Processed {processed}/{total}", + "upload_skipped_duplicates": "Skipped {count, plural, one {# duplicate asset} other {# duplicate assets}}", + "upload_status_duplicates": "Duplicates", + "upload_status_errors": "Errors", + "upload_status_uploaded": "Uploaded", + "upload_success": "Upload success, refresh the page to see new upload assets.", "url": "URL", "usage": "Usage", + "use_custom_date_range": "Use custom date range instead", "user": "User", "user_id": "User ID", + "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_role_set": "Set {user} as {role}", "user_usage_detail": "User usage detail", "username": "Username", "users": "Users", @@ -925,17 +1109,21 @@ "videos": "Videos", "videos_count": "{count, plural, one {# Video} other {# Videos}}", "view": "View", + "view_album": "View Album", "view_all": "View All", "view_all_users": "View all users", "view_links": "View links", "view_next_asset": "View next asset", "view_previous_asset": "View previous asset", - "viewer": "Viewer", + "view_stack": "View Stack", + "visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}", "waiting": "Waiting", + "warning": "Warning", "week": "Week", "welcome": "Welcome", "welcome_to_immich": "Welcome to immich", "year": "Year", + "years_ago": "{years, plural, one {# year} other {# years}} ago", "yes": "Yes", "you_dont_have_any_shared_links": "You don't have any shared links", "zoom_image": "Zoom Image" diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 0cb93bf6cc4ce..dfc6ab9127b23 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -197,25 +197,29 @@ export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileIm export const getPeopleThumbnailUrl = (personId: string) => createUrl(getPeopleThumbnailPath(personId)); -export const getAssetJobName = (job: AssetJobName) => { - const names: Record = { - [AssetJobName.RefreshMetadata]: 'Refresh metadata', - [AssetJobName.RegenerateThumbnail]: 'Refresh thumbnails', - [AssetJobName.TranscodeVideo]: 'Refresh encoded videos', +export const getAssetJobName = derived(t, ($t) => { + return (job: AssetJobName) => { + const names: Record = { + [AssetJobName.RefreshMetadata]: $t('refresh_metadata'), + [AssetJobName.RegenerateThumbnail]: $t('refresh_thumbnails'), + [AssetJobName.TranscodeVideo]: $t('refresh_encoded_videos'), + }; + + return names[job]; }; +}); - return names[job]; -}; +export const getAssetJobMessage = derived(t, ($t) => { + return (job: AssetJobName) => { + const messages: Record = { + [AssetJobName.RefreshMetadata]: $t('refreshing_metadata'), + [AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'), + [AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'), + }; -export const getAssetJobMessage = (job: AssetJobName) => { - const messages: Record = { - [AssetJobName.RefreshMetadata]: 'Refreshing metadata', - [AssetJobName.RegenerateThumbnail]: `Regenerating thumbnails`, - [AssetJobName.TranscodeVideo]: `Refreshing encoded video`, + return messages[job]; }; - - return messages[job]; -}; +}); export const getAssetJobIcon = (job: AssetJobName) => { const names: Record = { @@ -261,13 +265,14 @@ export const oauth = { return false; }, authorize: async (location: Location) => { + const $t = get(t); try { const redirectUri = location.href.split('?')[0]; const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } }); window.location.href = url; return true; } catch (error) { - handleError(error, 'Unable to login with OAuth'); + handleError(error, $t('errors.unable_to_login_with_oauth')); return false; } }, @@ -302,7 +307,10 @@ export const handlePromiseError = (promise: Promise): void => { export const s = (count: number) => (count === 1 ? '' : 's'); -export const memoryLaneTitle = (yearsAgo: number) => `${yearsAgo} year${s(yearsAgo)} ago`; +export const memoryLaneTitle = (yearsAgo: number) => { + const $t = get(t); + return $t('years_ago', { values: { years: yearsAgo } }); +}; export const withError = async (fn: () => Promise): Promise<[undefined, T] | [unknown, undefined]> => { try { diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index ecfd29a8fc40c..75232e793d6a0 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -1,5 +1,7 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import { handleError } from './handle-error'; export type OnDelete = (assetIds: string[]) => void; @@ -10,15 +12,18 @@ export type OnStack = (ids: string[]) => void; export type OnUnstack = (assets: AssetResponseDto[]) => void; export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { + const $t = get(t); try { await deleteBulk({ assetBulkDeleteDto: { ids, force } }); onAssetDelete(ids); notificationController.show({ - message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`, + message: force + ? $t('assets_permanently_deleted_count', { values: { count: ids.length } }) + : $t('assets_trashed_count', { values: { count: ids.length } }), type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Error deleting assets'); + handleError(error, $t('errors.unable_to_delete_assets')); } }; diff --git a/web/src/lib/utils/album-utils.ts b/web/src/lib/utils/album-utils.ts index cc0c3cbdc9b11..aff76ef88e2aa 100644 --- a/web/src/lib/utils/album-utils.ts +++ b/web/src/lib/utils/album-utils.ts @@ -1,6 +1,7 @@ import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; import { + AlbumFilter, AlbumGroupBy, AlbumSortBy, SortOrder, @@ -10,6 +11,7 @@ import { import { handleError } from '$lib/utils/handle-error'; import type { AlbumResponseDto } from '@immich/sdk'; import * as sdk from '@immich/sdk'; +import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; /** @@ -27,7 +29,8 @@ export const createAlbum = async (name?: string, assetIds?: string[]) => { }); return newAlbum; } catch (error) { - handleError(error, 'Failed to create album'); + const $t = get(t); + handleError(error, $t('errors.failed_to_create_album')); } }; @@ -45,7 +48,6 @@ export const createAlbumAndRedirect = async (name?: string, assetIds?: string[]) */ export interface AlbumSortOptionMetadata { id: AlbumSortBy; - text: string; defaultOrder: SortOrder; columnStyle: string; } @@ -53,37 +55,31 @@ export interface AlbumSortOptionMetadata { export const sortOptionsMetadata: AlbumSortOptionMetadata[] = [ { id: AlbumSortBy.Title, - text: 'Title', defaultOrder: SortOrder.Asc, columnStyle: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]', }, { id: AlbumSortBy.ItemCount, - text: 'Number of items', defaultOrder: SortOrder.Desc, columnStyle: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]', }, { id: AlbumSortBy.DateModified, - text: 'Date modified', defaultOrder: SortOrder.Desc, columnStyle: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', }, { id: AlbumSortBy.DateCreated, - text: 'Date created', defaultOrder: SortOrder.Desc, columnStyle: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', }, { id: AlbumSortBy.MostRecentPhoto, - text: 'Most recent photo', defaultOrder: SortOrder.Desc, columnStyle: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', }, { id: AlbumSortBy.OldestPhoto, - text: 'Oldest photo', defaultOrder: SortOrder.Desc, columnStyle: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', }, @@ -95,6 +91,12 @@ export const findSortOptionMetadata = (sortBy: string) => { return sortOptionsMetadata.find(({ id }) => sortBy === id) ?? defaultSortOption; }; +export const findFilterOption = (filter: string) => { + // Default is All filter + const defaultFilterOption = AlbumFilter.All; + return Object.values(AlbumFilter).find((key) => filter === AlbumFilter[key]) ?? defaultFilterOption; +}; + /** * -------------- * Album Grouping @@ -108,7 +110,6 @@ export interface AlbumGroup { export interface AlbumGroupOptionMetadata { id: AlbumGroupBy; - text: string; defaultOrder: SortOrder; isDisabled: () => boolean; } @@ -116,13 +117,11 @@ export interface AlbumGroupOptionMetadata { export const groupOptionsMetadata: AlbumGroupOptionMetadata[] = [ { id: AlbumGroupBy.None, - text: 'No grouping', defaultOrder: SortOrder.Asc, isDisabled: () => false, }, { id: AlbumGroupBy.Year, - text: 'Group by year', defaultOrder: SortOrder.Desc, isDisabled() { const disabledWithSortOptions: string[] = [AlbumSortBy.DateCreated, AlbumSortBy.DateModified]; @@ -131,7 +130,6 @@ export const groupOptionsMetadata: AlbumGroupOptionMetadata[] = [ }, { id: AlbumGroupBy.Owner, - text: 'Group by owner', defaultOrder: SortOrder.Asc, isDisabled: () => false, }, diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 3bacab5915f69..da80f3c3b8b35 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -6,7 +6,7 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; import { preferences } from '$lib/stores/user.store'; -import { downloadRequest, getKey, s, withError } from '$lib/utils'; +import { downloadRequest, getKey, withError } from '$lib/utils'; import { createAlbum } from '$lib/utils/album-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { encodeHTMLSpecialChars } from '$lib/utils/string-utils'; @@ -24,7 +24,7 @@ import { type UserResponseDto, } from '@immich/sdk'; import { DateTime } from 'luxon'; -import { t as translate } from 'svelte-i18n'; +import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import { handleError } from './handle-error'; @@ -37,15 +37,16 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[]) => { key: getKey(), }); const count = result.filter(({ success }) => success).length; + const $t = get(t); notificationController.show({ type: NotificationType.Info, timeout: 5000, message: count > 0 - ? `Added ${count} asset${s(count)} to the album` - : `Asset${assetIds.length === 1 ? ' was' : 's were'} already part of the album`, + ? $t('assets_added_to_album_count', { values: { count: count } }) + : $t('assets_were_part_of_album_count', { values: { count: assetIds.length } }), button: { - text: 'View Album', + text: $t('view_album'), onClick() { return goto(`${AppRoute.ALBUMS}/${albumId}`); }, @@ -59,13 +60,14 @@ export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[]) return; } const displayName = albumName ? `${encodeHTMLSpecialChars(albumName)}` : 'new album'; + const $t = get(t); notificationController.show({ type: NotificationType.Info, timeout: 5000, - message: `Added ${assetIds.length} asset${s(assetIds.length)} to ${displayName}`, + message: $t('assets_added_to_name_count', { values: { count: assetIds.length, name: displayName } }), html: true, button: { - text: 'View Album', + text: $t('view_album'), onClick() { return goto(`${AppRoute.ALBUMS}/${album.id}`); }, @@ -100,7 +102,8 @@ export const downloadArchive = async (fileName: string, options: Omit getDownloadInfo({ downloadInfoDto: dto, key: getKey() })); if (error) { - handleError(error, 'Unable to download files'); + const $t = get(t); + handleError(error, $t('errors.unable_to_download_files')); return; } @@ -134,7 +137,8 @@ export const downloadArchive = async (fileName: string, options: Omit { + const $t = get(t); if (asset.isOffline) { notificationController.show({ type: NotificationType.Info, - message: `Asset ${asset.originalFileName} is offline`, + message: $t('asset_filename_is_offline', { values: { filename: asset.originalFileName } }), }); return; } @@ -178,7 +183,7 @@ export const downloadFile = async (asset: AssetResponseDto) => { notificationController.show({ type: NotificationType.Info, - message: `Downloading asset ${asset.originalFileName}`, + message: $t('downloading_asset_filename', { values: { filename: asset.originalFileName } }), }); // TODO use sdk once it supports progress events @@ -191,7 +196,7 @@ export const downloadFile = async (asset: AssetResponseDto) => { downloadBlob(data, filename); } catch (error) { - handleError(error, `Error downloading ${filename}`); + handleError(error, $t('errors.error_downloading', { values: { filename: filename } })); downloadManager.clear(downloadKey); } finally { setTimeout(() => downloadManager.clear(downloadKey), 5000); @@ -302,8 +307,9 @@ export const getSelectedAssets = (assets: Set, user: UserRespo const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length; if (numberOfIssues > 0) { + const $t = get(t); notificationController.show({ - message: `Can't change metadata of ${numberOfIssues} asset${s(numberOfIssues)}`, + message: $t('errors.cant_change_metadata_assets_count', { values: { count: numberOfIssues } }), type: NotificationType.Warning, }); } @@ -318,6 +324,7 @@ export const stackAssets = async (assets: AssetResponseDto[]) => { const parent = assets[0]; const children = assets.slice(1); const ids = children.map(({ id }) => id); + const $t = get(t); try { await updateAssets({ @@ -327,7 +334,7 @@ export const stackAssets = async (assets: AssetResponseDto[]) => { }, }); } catch (error) { - handleError(error, 'Failed to stack assets'); + handleError(error, $t('errors.failed_to_stack_assets')); return false; } @@ -348,10 +355,10 @@ export const stackAssets = async (assets: AssetResponseDto[]) => { parent.stackCount = parent.stack.length + 1; notificationController.show({ - message: `Stacked ${parent.stackCount} assets`, + message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), type: NotificationType.Info, button: { - text: 'View Stack', + text: $t('view_stack'), onClick() { return assetViewingStore.setAssetId(parent.id); }, @@ -363,6 +370,7 @@ export const stackAssets = async (assets: AssetResponseDto[]) => { export const unstackAssets = async (assets: AssetResponseDto[]) => { const ids = assets.map(({ id }) => id); + const $t = get(t); try { await updateAssets({ assetBulkUpdateDto: { @@ -371,7 +379,7 @@ export const unstackAssets = async (assets: AssetResponseDto[]) => { }, }); } catch (error) { - handleError(error, 'Failed to un-stack assets'); + handleError(error, $t('errors.failed_to_unstack_assets')); return; } for (const asset of assets) { @@ -381,7 +389,7 @@ export const unstackAssets = async (assets: AssetResponseDto[]) => { } notificationController.show({ type: NotificationType.Info, - message: `Un-stacked ${assets.length} assets`, + message: $t('unstacked_assets_count', { values: { count: assets.length } }), }); return assets; }; @@ -409,12 +417,14 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt await delay(0); } } catch (error) { - handleError(error, 'Error selecting all assets'); + const $t = get(t); + handleError(error, $t('errors.error_selecting_all_assets')); isSelectingAllAssets.set(false); } }; export const toggleArchive = async (asset: AssetResponseDto) => { + const $t = get(t); try { const data = await updateAsset({ id: asset.id, @@ -427,10 +437,10 @@ export const toggleArchive = async (asset: AssetResponseDto) => { notificationController.show({ type: NotificationType.Info, - message: asset.isArchived ? `Added to archive` : `Removed from archive`, + message: asset.isArchived ? $t(`added_to_archive`) : $t(`removed_from_archive`), }); } catch (error) { - handleError(error, `Unable to ${asset.isArchived ? `remove asset from` : `add asset to`} archive`); + handleError(error, $t('errors.unable_to_add_remove_archive', { values: { archived: asset.isArchived } })); } return asset; @@ -439,6 +449,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => { export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => { const isArchived = archive; const ids = assets.map(({ id }) => id); + const $t = get(t); try { if (ids.length > 0) { @@ -449,13 +460,14 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean asset.isArchived = isArchived; } - const t = get(translate); notificationController.show({ - message: `${isArchived ? t('archived') : t('unarchived')} ${ids.length}`, + message: isArchived + ? $t('archived_count', { values: { count: ids.length } }) + : $t('unarchived_count', { values: { count: ids.length } }), type: NotificationType.Info, }); } catch (error) { - handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`); + handleError(error, $t('errors.unable_to_archive_unarchive', { values: { archived: isArchived } })); } return ids; diff --git a/web/src/lib/utils/person.ts b/web/src/lib/utils/person.ts index 018dbfbb8755a..06cddc9bdf03b 100644 --- a/web/src/lib/utils/person.ts +++ b/web/src/lib/utils/person.ts @@ -1,4 +1,6 @@ import type { PersonResponseDto } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; export const searchNameLocal = ( name: string, @@ -26,5 +28,6 @@ export const searchNameLocal = ( }; export const getPersonNameWithHiddenValue = (name: string, isHidden: boolean) => { - return `${name ? name + (isHidden ? ' ' : '') : ''}${isHidden ? '(hidden)' : ''}`; + const $t = get(t); + return $t('person_hidden', { values: { name: name, hidden: isHidden } }); }; diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index e73a0584a65dc..d581a440a88a7 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -37,10 +37,9 @@ import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; - import { locale } from '$lib/stores/preferences.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; - import { handlePromiseError, s } from '$lib/utils'; + import { handlePromiseError } from '$lib/utils'; import { downloadAlbum } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; @@ -168,10 +167,10 @@ }); notificationController.show({ type: NotificationType.Info, - message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`, + message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }), }); } catch (error) { - handleError(error, `Can't ${album.isActivityEnabled ? 'disable' : 'enable'} activity`); + handleError(error, $t('errors.cant_change_activity', { values: { enabled: album.isActivityEnabled } })); } }; @@ -189,7 +188,7 @@ reactions = [...reactions, isLiked]; } } catch (error) { - handleError(error, "Can't change favorite for asset"); + handleError(error, $t('errors.cant_change_asset_favorite')); } }; @@ -216,7 +215,7 @@ const { comments } = await getActivityStatistics({ albumId: album.id }); setNumberOfComments(comments); } catch (error) { - handleError(error, "Can't get number of comments"); + handleError(error, $t('errors.cant_get_number_of_comments')); } }; @@ -283,7 +282,7 @@ const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, - message: `Added ${count} asset${s(count)}`, + message: $t('assets_added_count', { values: { count: count } }), }); await refreshAlbum(); @@ -291,7 +290,7 @@ timelineInteractionStore.clearMultiselect(); viewMode = ViewMode.VIEW; } catch (error) { - handleError(error, 'Error adding assets to album'); + handleError(error, $t('errors.error_adding_assets_to_album')); } }; @@ -317,7 +316,7 @@ viewMode = ViewMode.VIEW; } catch (error) { - handleError(error, 'Error adding users to album'); + handleError(error, $t('errors.error_adding_users_to_album')); } }; @@ -331,7 +330,7 @@ await refreshAlbum(); viewMode = album.albumUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW; } catch (error) { - handleError(error, $t('errors.unable_to_load_album')); + handleError(error, $t('errors.error_deleting_shared_user')); } }; @@ -342,7 +341,7 @@ const handleRemoveAlbum = async () => { const isConfirmed = await dialogController.show({ id: 'remove-album', - prompt: `Are you sure you want to delete the album ${album.albumName}?\nIf this album is shared, other users will not be able to access it anymore.`, + prompt: $t('album_delete_confirmation', { values: { album: album.albumName } }), }); if (!isConfirmed) { @@ -393,7 +392,7 @@ }, }); } catch (error) { - handleError(error, 'Unable to update album cover'); + handleError(error, $t('errors.unable_to_update_album_cover')); } }; @@ -495,9 +494,9 @@

{#if $timelineSelected.size === 0} - Add to album + {$t('add_to_album')} {:else} - {$timelineSelected.size.toLocaleString($locale)} selected + {$t('selected_count', { values: { count: $timelineSelected.size } })} {/if}

@@ -508,7 +507,7 @@ on:click={handleSelectFromComputer} class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25" > - Select from computer + {$t('select_from_computer')} 0) { notificationController.show({ type: NotificationType.Error, - message: `Unable to change the visibility for ${results.length - count} ${ - results.length - count <= 1 ? 'person' : 'people' - }`, + message: $t('errors.unable_to_change_visibility', { values: { count: results.length - count } }), }); } notificationController.show({ type: NotificationType.Info, - message: `Visibility changed for ${count} ${count <= 1 ? 'person' : 'people'}`, + message: $t('visibility_changed', { values: { count: count } }), }); } } catch (error) { - handleError( - error, - `Unable to change the visibility for ${changed.length} ${changed.length <= 1 ? 'person' : 'people'}`, - ); + handleError(error, $t('errors.unable_to_change_visibility', { values: { count: changed.length } })); } // Reset variables used on the "Show & hide people" modal showLoadingSpinner = false; @@ -346,7 +341,7 @@ return person; }); notificationController.show({ - message: 'Date of birth saved successfully', + message: $t('birthdate_saved'), type: NotificationType.Info, }); } catch (error) { @@ -447,7 +442,7 @@

- {`No people${searchName ? ` named "${searchName}"` : ''}`} + {$t(searchName ? 'search_no_people_named' : 'search_no_people', { values: { name: searchName } })}

diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index d239da64c369b..eecfbf29b2632 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -30,7 +30,7 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { websocketEvents } from '$lib/stores/websocket'; - import { getPeopleThumbnailUrl, handlePromiseError, s } from '$lib/utils'; + import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; import { clickOutside } from '$lib/actions/click-outside'; import { handleError } from '$lib/utils/handle-error'; import { isExternalUrl } from '$lib/utils/navigation'; @@ -488,7 +488,7 @@ {#if data.person.name}

{data.person.name}

- {`${numberOfAssets} asset${s(numberOfAssets)}`} + {$t('assets_count', { values: { count: numberOfAssets } })}

{:else}

{$t('add_a_name')}

diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7723a86b41102..9eb7d765466e2 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -279,7 +279,9 @@
{$t('albums').toUpperCase()}
-
PHOTOS & VIDEOS
+
+ {$t('photos_and_videos').toUpperCase()} +
{/if}
@@ -296,7 +298,7 @@

{$t('no_results')}

-

Try a synonym or more general keyword

+

{$t('no_results_description')}

{/if} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index baef91b39d8e1..40dd3b5d313a6 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -37,8 +37,7 @@ const handleEmptyTrash = async () => { const isConfirmed = await dialogController.show({ id: 'empty-trash', - prompt: - 'Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!', + prompt: $t('empty_trash_confirmation'), }); if (!isConfirmed) { @@ -53,7 +52,7 @@ assetStore.removeAssets(deletedAssetIds); notificationController.show({ - message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, + message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }), type: NotificationType.Info, }); } catch (error) { @@ -64,7 +63,7 @@ const handleRestoreTrash = async () => { const isConfirmed = await dialogController.show({ id: 'restore-trash', - prompt: 'Are you sure you want to restore all your trashed assets? You cannot undo this action!', + prompt: $t('assets_restore_confirmation'), }); if (!isConfirmed) { @@ -78,7 +77,7 @@ assetStore.removeAssets(restoredAssetIds); notificationController.show({ - message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, + message: $t('assets_restored_count', { values: { count: numberOfAssets } }), type: NotificationType.Info, }); } catch (error) { diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3e0ab8bb607c5..dc614d0f0ec82 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -42,8 +42,8 @@ notificationController.show({ message: $featureFlags.trash - ? $t('assets_moved_to_trash', { values: { count: trashedCount } }) - : $t('permanently_deleted_assets', { values: { count: trashedCount } }), + ? $t('assets_moved_to_trash_count', { values: { count: trashedCount } }) + : $t('permanently_deleted_assets_count', { values: { count: trashedCount } }), type: NotificationType.Info, }); }; diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte index e2a809c6d58a5..e82605d83ef9a 100644 --- a/web/src/routes/+error.svelte +++ b/web/src/routes/+error.svelte @@ -35,7 +35,7 @@

- 🚨 Error - Something went wrong + 🚨 {$t('error_title')}

- Hi {$user.name} ({$user.email}), + {$t('hi_user', { values: { name: $user.name, email: $user.email } })}

- This is either the first time you are signing into the system or a request has been made to change your password. Please - enter the new password below. + {$t('change_password_description')}

diff --git a/web/src/routes/auth/change-password/+page.ts b/web/src/routes/auth/change-password/+page.ts index 4c9fc0cb7efe5..0e961d253cb50 100644 --- a/web/src/routes/auth/change-password/+page.ts +++ b/web/src/routes/auth/change-password/+page.ts @@ -2,6 +2,7 @@ import { AppRoute } from '$lib/constants'; import { user } from '$lib/stores/user.store'; import { authenticate } from '$lib/utils/auth'; import { redirect } from '@sveltejs/kit'; +import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import type { PageLoad } from './$types'; @@ -11,9 +12,11 @@ export const load = (async () => { redirect(302, AppRoute.PHOTOS); } + const $t = get(t); + return { meta: { - title: 'Change Password', + title: $t('change_password'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index e308e117f795b..397b945661b35 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,6 +1,8 @@ import { AppRoute } from '$lib/constants'; import { defaults, getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async ({ fetch }) => { @@ -11,9 +13,10 @@ export const load = (async ({ fetch }) => { redirect(302, AppRoute.AUTH_REGISTER); } + const $t = get(t); return { meta: { - title: 'Login', + title: $t('login'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index 4eb54e6095452..c578acc2e7e09 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -1,13 +1,18 @@ import { loadConfig } from '$lib/stores/server-config.store'; import { authenticate } from '$lib/utils/auth'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); await loadConfig(); + + const $t = get(t); + return { meta: { - title: 'Onboarding', + title: $t('onboarding'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/auth/register/+page.svelte b/web/src/routes/auth/register/+page.svelte index c5db69f043a25..9c1f0ca6c49d4 100644 --- a/web/src/routes/auth/register/+page.svelte +++ b/web/src/routes/auth/register/+page.svelte @@ -2,14 +2,14 @@ import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte'; import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import type { PageData } from './$types'; + import { t } from 'svelte-i18n'; export let data: PageData;

- Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative - tasks, and additional users will be created by you. + {$t('admin.registration_description')}

diff --git a/web/src/routes/auth/register/+page.ts b/web/src/routes/auth/register/+page.ts index cda5d9d3f8fd7..9ecf1fda967d5 100644 --- a/web/src/routes/auth/register/+page.ts +++ b/web/src/routes/auth/register/+page.ts @@ -1,6 +1,8 @@ import { AppRoute } from '$lib/constants'; import { getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { @@ -10,9 +12,11 @@ export const load = (async () => { redirect(302, AppRoute.AUTH_LOGIN); } + const $t = get(t); + return { meta: { - title: 'Admin Registration', + title: $t('admin.registration'), }, }; }) satisfies PageLoad;