From dd2c7400a693937e715f5d585e872b2c1f0534ba Mon Sep 17 00:00:00 2001 From: waclaw66 <waclaw66@seznam.cz> Date: Mon, 24 Jun 2024 15:50:01 +0200 Subject: [PATCH] chore(web): another missing translations (#10274) * chore(web): another missing translations * unused removed * more keys * lint fix * test fixed * dynamic translation fix * fixes * people search translation * params fixed * keep filter setting fix * lint fix * $t fixes * Update web/src/lib/i18n/en.json Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * another missing * activity translation * link sharing translations * expiration dropdown fix - didn't work localized * notification title * device logout * search results * reset to default * unsaved change * select from computer * selected * select-2 * select-3 * unmerge * pluralize, force icu message * Update web/src/lib/components/asset-viewer/asset-viewer.svelte Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * review fixes * remove user * plural fixes * ffmpeg settings * fixes * error title * plural fixes * onboarding * change password * more more * console log fix * another * api key desc * map marker * format fix * key fix * asset-utils * utils * misc --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> --- .../settings/ffmpeg/ffmpeg-settings.svelte | 6 +- .../album-page/__tests__/album-card.spec.ts | 9 +- .../album-page/album-card-group.svelte | 3 +- .../components/album-page/album-card.svelte | 7 +- .../album-page/album-options.svelte | 2 +- .../album-page/album-summary.svelte | 3 +- .../album-page/albums-controls.svelte | 54 +++- .../components/album-page/albums-list.svelte | 4 +- .../album-page/albums-table-header.svelte | 16 +- .../album-page/albums-table-row.svelte | 4 +- .../components/album-page/albums-table.svelte | 4 +- .../album-page/share-info-modal.svelte | 23 +- .../album-page/thumbnail-selection.svelte | 2 +- .../album-page/user-selection-modal.svelte | 8 +- .../asset-viewer/activity-viewer.svelte | 17 +- .../album-list-item-details.svelte | 5 +- .../asset-viewer/asset-viewer-nav-bar.svelte | 6 +- .../asset-viewer/asset-viewer.svelte | 16 +- .../asset-viewer/detail-panel.svelte | 15 +- .../faces-page/merge-face-selector.svelte | 4 +- .../faces-page/merge-suggestion-modal.svelte | 4 +- .../faces-page/people-search.svelte | 2 +- .../faces-page/person-side-panel.svelte | 10 +- .../faces-page/set-birth-date-modal.svelte | 2 +- .../faces-page/unmerge-face-selector.svelte | 25 +- .../forms/admin-registration-form.svelte | 2 +- .../lib/components/forms/api-key-form.svelte | 2 +- .../components/forms/api-key-secret.svelte | 2 +- .../forms/change-password-form.svelte | 2 +- .../components/forms/edit-album-form.svelte | 2 +- .../lib/components/forms/login-form.svelte | 12 +- .../map-page/map-settings-modal.svelte | 6 +- .../onboarding-page/onboarding-hello.svelte | 4 +- .../onboarding-storage-template.svelte | 2 +- .../onboarding-page/onboarding-theme.svelte | 2 +- .../actions/asset-job-actions.svelte | 4 +- .../actions/favorite-action.svelte | 6 +- .../actions/remove-from-album.svelte | 7 +- .../actions/remove-from-shared-link.svelte | 12 +- .../photos-page/actions/restore-assets.svelte | 2 +- .../asset-select-control-bar.svelte | 4 +- .../photos-page/delete-asset-dialog.svelte | 7 +- .../individual-shared-viewer.svelte | 4 +- .../album-selection-modal.svelte | 7 +- .../shared-components/change-location.svelte | 2 +- .../shared-components/combobox.svelte | 2 +- .../create-shared-link-modal.svelte | 17 +- .../dialog/confirm-dialog.svelte | 2 +- .../drag-and-drop-upload-overlay.svelte | 3 +- .../gallery-viewer/gallery-viewer.svelte | 5 +- .../shared-components/map/map.svelte | 4 +- .../__tests__/notification-card.spec.ts | 2 +- .../notification/notification-card.svelte | 4 +- .../profile-image-cropper.svelte | 2 +- .../search-bar/search-display-section.svelte | 2 +- .../search-bar/search-people-section.svelte | 6 +- .../settings/setting-buttons-row.svelte | 2 +- .../settings/setting-checkboxes.svelte | 3 +- .../settings/setting-combobox.svelte | 3 +- .../settings/setting-dropdown.svelte | 3 +- .../settings/setting-input-field.svelte | 3 +- .../settings/setting-select.svelte | 3 +- .../settings/setting-switch.svelte | 3 +- .../settings/setting-textarea.svelte | 3 +- .../shared-components/show-shortcuts.svelte | 4 +- .../shared-components/upload-panel.svelte | 21 +- .../version-announcement-box.svelte | 6 +- .../sharedlinks-page/shared-link-card.svelte | 20 +- .../user-settings-page/device-list.svelte | 12 +- .../notifications-settings.svelte | 4 +- .../partner-selection-modal.svelte | 2 +- web/src/lib/i18n/en.json | 234 ++++++++++++++++-- web/src/lib/utils.ts | 42 ++-- web/src/lib/utils/actions.ts | 9 +- web/src/lib/utils/album-utils.ts | 22 +- web/src/lib/utils/asset-utils.ts | 60 +++-- web/src/lib/utils/person.ts | 5 +- .../[[assetId=id]]/+page.svelte | 29 ++- web/src/routes/(user)/people/+page.svelte | 15 +- .../[[assetId=id]]/+page.svelte | 4 +- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 9 +- .../[[assetId=id]]/+page.svelte | 4 +- web/src/routes/+error.svelte | 2 +- .../routes/auth/change-password/+page.svelte | 6 +- web/src/routes/auth/change-password/+page.ts | 5 +- web/src/routes/auth/login/+page.ts | 5 +- web/src/routes/auth/onboarding/+page.ts | 7 +- web/src/routes/auth/register/+page.svelte | 4 +- web/src/routes/auth/register/+page.ts | 6 +- 90 files changed, 635 insertions(+), 322 deletions(-) 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 caef2d5cbb..c39ea75717 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 977899db82..6ffa273a4d 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<AlbumCard>; + 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 d70965c9f6..0e731a683c 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}" /> <span class="font-bold text-3xl text-black dark:text-white">{group.name}</span> - <span class="ml-1.5">({albums.length} {albums.length > 1 ? 'albums' : 'album'})</span> + <span class="ml-1.5">({$t('albums_count', { values: { count: albums.length } })})</span> </button> <hr class="dark:border-immich-dark-gray" /> </div> diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index b536933738..60eda166ab 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 @@ <script lang="ts"> - import { locale } from '$lib/stores/preferences.store'; import { user } from '$lib/stores/user.store'; import type { AlbumResponseDto } from '@immich/sdk'; import { mdiDotsVertical } from '@mdi/js'; @@ -7,7 +6,6 @@ import { getShortDateRange } from '$lib/utils/date-time'; import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { s } from '$lib/utils'; import { t } from 'svelte-i18n'; export let album: AlbumResponseDto; @@ -66,8 +64,7 @@ <span class="flex gap-2 text-sm dark:text-immich-dark-fg" data-testid="album-details"> {#if showItemCount} <p> - {album.assetCount.toLocaleString($locale)} - item{s(album.assetCount)} + {$t('items_count', { values: { count: album.assetCount } })} </p> {/if} @@ -79,7 +76,7 @@ {#if $user.id === album.ownerId} <p>{$t('owned')}</p> {:else if album.owner} - <p>Shared by {album.owner.name}</p> + <p>{$t('shared_by_user', { values: { user: album.owner.name } })}</p> {:else} <p>{$t('shared')}</p> {/if} diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 8948073138..84a2873788 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -65,7 +65,7 @@ /> {/if} <SettingSwitch - title="Comments & likes" + title={$t('comments_and_likes')} subtitle={$t('let_others_respond')} checked={album.isActivityEnabled} on:toggle={() => dispatch('toggleEnableActivity')} diff --git a/web/src/lib/components/album-page/album-summary.svelte b/web/src/lib/components/album-page/album-summary.svelte index c1832544f8..0277035d5c 100644 --- a/web/src/lib/components/album-page/album-summary.svelte +++ b/web/src/lib/components/album-page/album-summary.svelte @@ -2,6 +2,7 @@ import { dateFormats } from '$lib/constants'; import { locale } from '$lib/stores/preferences.store'; import type { AlbumResponseDto } from '@immich/sdk'; + import { t } from 'svelte-i18n'; export let album: AlbumResponseDto; @@ -28,5 +29,5 @@ <span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details"> <span>{getDateRange(startDate, endDate)}</span> <span>•</span> - <span>{album.assetCount} items</span> + <span>{$t('items_count', { values: { count: album.assetCount } })}</span> </span> diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte index 0657221dd4..793c2b4970 100644 --- a/web/src/lib/components/album-page/albums-controls.svelte +++ b/web/src/lib/components/album-page/albums-controls.svelte @@ -4,6 +4,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import { AlbumFilter, + AlbumSortBy, AlbumGroupBy, AlbumViewMode, albumViewSettings, @@ -25,6 +26,7 @@ type AlbumGroupOptionMetadata, type AlbumSortOptionMetadata, findGroupOptionMetadata, + findFilterOption, findSortOptionMetadata, getSelectedAlbumGroupOption, groupOptionsMetadata, @@ -43,6 +45,11 @@ return ordering === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc; }; + const handleChangeAlbumFilter = (filter: string, defaultFilter: AlbumFilter) => { + $albumViewSettings.filter = + Object.keys(albumFilterNames).find((key) => albumFilterNames[key as AlbumFilter] === filter) ?? defaultFilter; + }; + const handleChangeGroupBy = ({ id, defaultOrder }: AlbumGroupOptionMetadata) => { if ($albumViewSettings.groupBy === id) { $albumViewSettings.groupOrder = flipOrdering($albumViewSettings.groupOrder); @@ -69,6 +76,10 @@ let selectedGroupOption: AlbumGroupOptionMetadata; let groupIcon: string; + $: selectedFilterOption = albumFilterNames[findFilterOption($albumViewSettings.filter)]; + + $: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy); + $: { selectedGroupOption = findGroupOptionMetadata($albumViewSettings.groupBy); if (selectedGroupOption.isDisabled()) { @@ -76,8 +87,6 @@ } } - $: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy); - $: { if (selectedGroupOption.id === AlbumGroupBy.None) { groupIcon = mdiFolderRemoveOutline; @@ -88,14 +97,41 @@ } $: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin; + + $: albumFilterNames = ((): Record<AlbumFilter, string> => { + return { + [AlbumFilter.All]: $t('all'), + [AlbumFilter.Owned]: $t('owned'), + [AlbumFilter.Shared]: $t('shared'), + }; + })(); + + $: albumSortByNames = ((): Record<AlbumSortBy, string> => { + return { + [AlbumSortBy.Title]: $t('sort_title'), + [AlbumSortBy.ItemCount]: $t('sort_items'), + [AlbumSortBy.DateModified]: $t('sort_modified'), + [AlbumSortBy.DateCreated]: $t('sort_created'), + [AlbumSortBy.MostRecentPhoto]: $t('sort_recent'), + [AlbumSortBy.OldestPhoto]: $t('sort_oldest'), + }; + })(); + + $: albumGroupByNames = ((): Record<AlbumGroupBy, string> => { + return { + [AlbumGroupBy.None]: $t('group_no'), + [AlbumGroupBy.Owner]: $t('group_owner'), + [AlbumGroupBy.Year]: $t('group_year'), + }; + })(); </script> <!-- Filter Albums by Sharing Status (All, Owned, Shared) --> <div class="hidden xl:block h-10"> <GroupTab - filters={Object.keys(AlbumFilter)} - selected={$albumViewSettings.filter} - onSelect={(selected) => ($albumViewSettings.filter = selected)} + filters={Object.values(albumFilterNames)} + selected={selectedFilterOption} + onSelect={(selected) => handleChangeAlbumFilter(selected, AlbumFilter.All)} /> </div> @@ -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 1f5d654c4a..740b3876bd 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 527b1822e0..2c396bebed 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 @@ <script lang="ts"> - import { albumViewSettings, SortOrder } from '$lib/stores/preferences.store'; + import { albumViewSettings, SortOrder, AlbumSortBy } from '$lib/stores/preferences.store'; import type { AlbumSortOptionMetadata } from '$lib/utils/album-utils'; + import { t } from 'svelte-i18n'; export let option: AlbumSortOptionMetadata; @@ -12,6 +13,17 @@ $albumViewSettings.sortOrder = option.defaultOrder; } }; + + $: albumSortByNames = ((): Record<AlbumSortBy, string> => { + return { + [AlbumSortBy.Title]: $t('sort_title'), + [AlbumSortBy.ItemCount]: $t('sort_items'), + [AlbumSortBy.DateModified]: $t('sort_modified'), + [AlbumSortBy.DateCreated]: $t('sort_created'), + [AlbumSortBy.MostRecentPhoto]: $t('sort_recent'), + [AlbumSortBy.OldestPhoto]: $t('sort_oldest'), + }; + })(); </script> <th class="text-sm font-medium {option.columnStyle}"> @@ -27,6 +39,6 @@ ↑ {/if} {/if} - {option.text} + {albumSortByNames[option.id]} </button> </th> 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 b85b9d1229..f91628ec63 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} </td> diff --git a/web/src/lib/components/album-page/albums-table.svelte b/web/src/lib/components/album-page/albums-table.svelte index 0c33584eb1..30164ef6ae 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 @@ /> <span class="font-bold text-2xl">{albumGroup.name}</span> <span class="ml-1.5"> - ({albumGroup.albums.length} - {albumGroup.albums.length > 1 ? 'albums' : 'album'}) + ({$t('albums_count', { values: { count: albumGroup.albums.length } })}) </span> </td> </tr> 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 3bda154249..c9ac224992 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 @@ <div id="icon-{user.id}" class="flex place-items-center gap-2 text-sm"> <div> {#if role === AlbumUserRole.Viewer} - Viewer + {$t('role_viewer')} {:else} - Editor + {$t('role_editor')} {/if} </div> {#if isOwned} @@ -135,8 +140,8 @@ {#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id} <ConfirmDialog - title="Leave album?" - prompt="Are you sure you want to leave {album.albumName}?" + title={$t('album_leave')} + prompt={$t('album_leave_confirmation', { values: { album: album.albumName } })} confirmText={$t('leave')} onConfirm={handleRemoveUser} onCancel={() => (selectedRemoveUser = null)} @@ -145,9 +150,9 @@ {#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id} <ConfirmDialog - title="Remove user?" - prompt="Are you sure you want to remove {selectedRemoveUser.name}?" - confirmText={$t('remove')} + title={$t('album_remove_user')} + prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })} + confirmText={$t('remove_user')} onConfirm={handleRemoveUser} onCancel={() => (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 9f964c4ada..9e6c786d22 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')} </Button> </svelte:fragment> </ControlAppBar> 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 1e8dd56d9c..5521d52173 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<string, { user: UserResponseDto; role: AlbumUserRole }> = {}; 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} <p class="p-5 text-sm"> - 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')} </p> {/if} diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index a4cc7cf89d..4664dcc3c5 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, string> = { + [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 @@ <div class="text-red-600"><Icon path={mdiHeart} size={20} /></div> <div class="w-full" title={`${reaction.user.name} (${reaction.user.email})`}> - {`${reaction.user.name} liked ${assetType ? `this ${getAssetType(assetType).toLowerCase()}` : 'it'}`} + {$t('user_liked', { + values: { + user: reaction.user.name, + type: assetType ? getAssetType(assetType).toLowerCase() : null, + }, + })} </div> {#if assetId === undefined && reaction.assetId} <a diff --git a/web/src/lib/components/asset-viewer/album-list-item-details.svelte b/web/src/lib/components/asset-viewer/album-list-item-details.svelte index 60e49526ef..ecc38b7c24 100644 --- a/web/src/lib/components/asset-viewer/album-list-item-details.svelte +++ b/web/src/lib/components/asset-viewer/album-list-item-details.svelte @@ -1,10 +1,11 @@ <script lang="ts"> import type { AlbumResponseDto } from '@immich/sdk'; + import { t } from 'svelte-i18n'; export let album: AlbumResponseDto; </script> -<span>{album.assetCount} items</span> +<span>{$t('items_count', { values: { count: album.assetCount } })}</span> {#if album.shared} - <span>• Shared</span> + <span>• {$t('shared')}</span> {/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 d891051d78..fc1239d396 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 @@ <MenuOption icon={mdiDatabaseRefreshOutline} onClick={() => onJobClick(AssetJobName.RefreshMetadata)} - text={getAssetJobName(AssetJobName.RefreshMetadata)} + text={$getAssetJobName(AssetJobName.RefreshMetadata)} /> <MenuOption icon={mdiImageRefreshOutline} onClick={() => onJobClick(AssetJobName.RegenerateThumbnail)} - text={getAssetJobName(AssetJobName.RegenerateThumbnail)} + text={$getAssetJobName(AssetJobName.RegenerateThumbnail)} /> {#if asset.type === AssetTypeEnum.Video} <MenuOption icon={mdiCogRefreshOutline} onClick={() => 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 0225a92df2..e3cafae14c 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 5414871adb..ac3a4b4c9e 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 @@ <div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">{$t('asset_offline')}</div> <div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700"> <p> - 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')} </p> </div> </div> @@ -170,8 +169,8 @@ <div class="flex gap-2 items-center"> {#if unassignedFaces.length > 0} <Icon - ariaLabel="Asset has unassigned faces" - title="Asset has unassigned faces" + ariaLabel={$t('asset_has_unassigned_faces')} + title={$t('asset_has_unassigned_faces')} color="currentColor" path={mdiAccountOff} size="24" @@ -243,11 +242,11 @@ )} > {#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} </p> {/if} @@ -452,7 +451,7 @@ target="_blank" class="font-medium text-immich-primary" > - Open in OpenStreetMap + {$t('open_in_openstreetmap')} </a> </div> </svelte:fragment> 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 6166c38caa..2ac78c990d 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 @@ <ControlAppBar on:close={onClose}> <svelte:fragment slot="leading"> {#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 9621f6862f..3f8a8b36d2 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 @@ </div> <div class="flex px-4 md:pt-4"> - <h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1> + <h1 class="text-xl text-gray-500 dark:text-gray-300">{$t('are_these_the_same_person')}</h1> </div> <div class="flex px-4 pt-2"> - <p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p> + <p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p> </div> <svelte:fragment slot="sticky-bottom"> <Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button> diff --git a/web/src/lib/components/faces-page/people-search.svelte b/web/src/lib/components/faces-page/people-search.svelte index ed33ded46b..cfd4c8f29a 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 a25e303103..a848a17e75 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')} </button> {:else} <LoadingSpinner /> @@ -299,7 +299,7 @@ <CircleIconButton color="primary" icon={mdiRestart} - title="Reset" + title={$t('reset')} size="18" padding="1" class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform" diff --git a/web/src/lib/components/faces-page/set-birth-date-modal.svelte b/web/src/lib/components/faces-page/set-birth-date-modal.svelte index 1d2e0d9a04..b670f34dfd 100644 --- a/web/src/lib/components/faces-page/set-birth-date-modal.svelte +++ b/web/src/lib/components/faces-page/set-birth-date-modal.svelte @@ -24,7 +24,7 @@ <FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}> <div class="text-immich-primary dark:text-immich-dark-primary"> <p class="text-sm dark:text-immich-dark-fg"> - Date of birth is used to calculate the age of this person at the time of a photo. + {$t('birthdate_set_description')} </p> </div> 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 1e480f85db..c89c8338d3 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 @@ <svelte:fragment slot="trailing"> <div class="flex gap-4"> <Button - title={'Assign selected assets to a new person'} + title={$t('create_new_person_hint')} size={'sm'} disabled={disableButtons || hasSelection} on:click={handleCreate} @@ -138,11 +141,11 @@ {:else} <LoadingSpinner /> {/if} - <span class="ml-2"> Create new Person</span></Button + <span class="ml-2"> {$t('create_new_person')}</span></Button > <Button size={'sm'} - title={'Assign selected assets to an existing person'} + title={$t('reassing_hint')} disabled={disableButtons || !hasSelection} on:click={handleReassign} > @@ -153,7 +156,7 @@ {:else} <LoadingSpinner /> {/if} - <span class="ml-2"> Reassign</span></Button + <span class="ml-2"> {$t('reassign')}</span></Button > </div> </svelte:fragment> diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte index 54667cd300..c66b09040f 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 9273faf38e..55ec258b40 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 1722029ae7..b7bf8e1836 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 @@ <FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}> <div class="text-immich-primary dark:text-immich-dark-primary"> <p class="text-sm dark:text-immich-dark-fg"> - This value will only be shown once. Please be sure to copy it before closing the window. + {$t('api_key_description')} </p> </div> diff --git a/web/src/lib/components/forms/change-password-form.svelte b/web/src/lib/components/forms/change-password-form.svelte index add87bb0ab..799dde7ef3 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 @@ <p class="text-sm text-immich-primary">{success}</p> {/if} <div class="my-5 flex w-full"> - <Button type="submit" size="lg" fullwidth>{$t('change_password')}</Button> + <Button type="submit" size="lg" fullwidth>{$t('to_change_password')}</Button> </div> </form> diff --git a/web/src/lib/components/forms/edit-album-form.svelte b/web/src/lib/components/forms/edit-album-form.svelte index 0c3b8e1044..bcb097eca6 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 9bfffa90bb..828927a13a 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'); } }; </script> @@ -124,7 +124,7 @@ <LoadingSpinner /> </span> {:else} - Login + {$t('to_login')} {/if} </Button> </div> @@ -138,7 +138,7 @@ <span class="absolute left-1/2 -translate-x-1/2 bg-white px-3 font-medium text-gray-900 dark:bg-immich-dark-gray dark:text-white" > - or + {$t('or')} </span> </div> {/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 ccbdc9b03c..b442396c84 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')} </LinkButton> </div> </div> @@ -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')} </LinkButton> </div> </div> diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index a3c7a0b93d..c2d318ccda 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 @@ <OnboardingCard> <ImmichLogo noText width="75" /> <p class="font-medium text-6xl my-6 text-immich-primary dark:text-immich-dark-primary"> - Welcome, {$user.name} + {$t('onboarding_welcome_user', { values: { user: $user.name } })} </p> - <p class="text-3xl pb-6 font-light">Let's get your instance set up with some common settings.</p> + <p class="text-3xl pb-6 font-light">{$t('onboarding_welcome_description')}</p> <div class="w-full flex place-content-end"> <Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}> diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index e8f3542f5d..74ddb6459c 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -61,7 +61,7 @@ }} > <span class="flex place-content-center place-items-center gap-2"> - Done + {$t('done')} <Icon path={mdiCheck} size="18" /> </span> </Button> diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index 2f2928ab4d..ff15b8b64a 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 @@ <p class="text-xl text-immich-primary dark:text-immich-dark-primary">{$t('color_theme').toUpperCase()}</p> <div> - <p class="pb-6 font-light">Choose a color theme for your instance. You can change this later in your settings.</p> + <p class="pb-6 font-light">{$t('onboarding_theme_description')}</p> </div> <div class="flex gap-4 mb-6"> 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 4460548c38..ca61d54d43 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} - <MenuOption text={getAssetJobName(job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} /> + <MenuOption text={$getAssetJobName(job)} icon={getAssetJobIcon(job)} onClick={() => 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 503f0ba156..1d723b1a9d 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 90bdb39b71..251706a8c5 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 ea8c3e3bec..f687792bd5 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 @@ <script lang="ts"> import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { getKey, s } from '$lib/utils'; + import { getKey } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { removeSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk'; import { mdiDeleteOutline } from '@mdi/js'; @@ -16,9 +16,9 @@ const handleRemove = async () => { const isConfirmed = await dialogController.show({ id: 'remove-from-shared-link', - title: 'Remove assets?', - prompt: `Are you sure you want to remove ${getAssets().size} asset${s(getAssets().size)} from this shared link?`, - confirmText: 'Remove', + title: $t('remove_assets_title'), + prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().size } }), + confirmText: $t('remove'), }); if (!isConfirmed) { @@ -46,12 +46,12 @@ notificationController.show({ type: NotificationType.Info, - message: `Removed ${count} assets`, + message: $t('assets_removed_count', { values: { count: count } }), }); clearSelect(); } catch (error) { - handleError(error, 'Unable to remove assets from shared link'); + handleError(error, $t('errors.unable_to_remove_assets_from_shared_link')); } }; </script> 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 36f8d214f4..19e1c206fd 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 1a2af7b955..060aa89f19 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 @@ </script> <script lang="ts"> - import { locale } from '$lib/stores/preferences.store'; import type { AssetResponseDto } from '@immich/sdk'; import { mdiClose } from '@mdi/js'; import ControlAppBar from '../shared-components/control-app-bar.svelte'; @@ -33,8 +32,7 @@ <ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> <p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading"> - {$t('selected')} - {assets.size.toLocaleString($locale)} + {$t('selected_count', { values: { count: assets.size } })} </p> <slot slot="trailing" /> </ControlAppBar> diff --git a/web/src/lib/components/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte index 70870362aa..56ab150b4e 100644 --- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte +++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte @@ -3,7 +3,6 @@ import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; import Checkbox from '$lib/components/elements/checkbox.svelte'; - import { s } from '$lib/utils'; import { t } from 'svelte-i18n'; export let size: number; @@ -24,7 +23,7 @@ </script> <ConfirmDialog - title="Permanently delete asset{s(size)}" + title={$t('permanently_delete_assets_count', { values: { count: size } })} confirmText={$t('delete')} onConfirm={handleConfirm} onCancel={() => dispatch('cancel')} @@ -38,10 +37,10 @@ this asset? This will also remove it from its album(s). {/if} </p> - <p><b>You cannot undo this action!</b></p> + <p><b>{$t('cannot_undo_this_action')}</b></p> <div class="pt-4 flex justify-center items-center"> - <Checkbox id="confirm-deletion-input" label="Do not show this message again" bind:checked /> + <Checkbox id="confirm-deletion-input" label={$t('do_not_show_again')} bind:checked /> </div> </svelte:fragment> </ConfirmDialog> 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 831c98db1d..af5c54c988 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 85d68b7264..fb6e6788bc 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} <p class="px-5 py-3 text-xs"> - {#if search.length === 0}ALL - {/if}ALBUMS + {(search.length === 0 ? $t('all_albums') : $t('albums')).toUpperCase()} </p> {/if} {#each filteredAlbums as album (album.id)} <AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} /> {/each} {:else if albums.length > 0} - <p class="px-5 py-1 text-sm">It looks like you do not have any albums with this name yet.</p> + <p class="px-5 py-1 text-sm">{$t('no_albums_with_name_yet')}</p> {:else} - <p class="px-5 py-1 text-sm">It looks like you do not have any albums yet.</p> + <p class="px-5 py-1 text-sm">{$t('no_albums_yet')}</p> {/if} </div> {/if} diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index dfff248816..9feebe91ff 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 7e6e3f5423..c212849a5b 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')} </li> {/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 512b415a6b..97c3aaf17e 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 @@ <section> {#if shareType === SharedLinkType.Album} {#if !editingLink} - <div>Let anyone with the link see photos and people in this album.</div> + <div>{$t('album_with_link_access')}</div> {:else} <div class="text-sm"> - Public album | <span class="text-immich-primary dark:text-immich-dark-primary" - >{editingLink.album?.albumName}</span - > + {$t('public_album')} | + <span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.album?.albumName}</span> </div> {/if} {/if} {#if shareType === SharedLinkType.Individual} {#if !editingLink} - <div>Let anyone with the link see the selected photo(s)</div> + <div>{$t('create_link_to_share_description')}</div> {:else} <div class="text-sm"> {$t('individual_share')} | @@ -204,13 +203,13 @@ <div class="my-3"> <SettingSwitch bind:checked={allowDownload} - title={'Allow public user to download'} + title={$t('allow_public_user_to_download')} disabled={!showMetadata} /> </div> <div class="my-3"> - <SettingSwitch bind:checked={allowUpload} title={'Allow public user to upload'} /> + <SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} /> </div> <div class="text-sm"> 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 6acd819533..50d5fe56ce 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 dc115c45b4..466b3d083e 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 @@ }} > <ImmichLogo noText class="m-16 w-48 animate-bounce" /> - <div class="text-2xl">Drop files anywhere to upload</div> + <div class="text-2xl">{$t('drop_files_to_upload')}</div> </div> {/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 647b695c7b..819105e197 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 c43424db07..91f8146631 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 76b9c39564..187b1e0ccb 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 6ddeb7433e..0919bca035 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 @@ <div class="flex place-items-center gap-2"> <Icon path={icon} color={primaryColor[notification.type]} size="20" /> <h2 style:color={primaryColor[notification.type]} class="font-medium" data-testid="title"> - {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} </h2> </div> <CircleIconButton diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte index 33fa1ee8e8..0c3f79895e 100644 --- a/web/src/lib/components/shared-components/profile-image-cropper.svelte +++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte @@ -50,7 +50,7 @@ if (await hasTransparentPixels(blob)) { notificationController.show({ type: NotificationType.Error, - message: 'Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.', + message: $t('errors.profile_picture_transparent_pixels'), timeout: 3000, }); return; diff --git a/web/src/lib/components/shared-components/search-bar/search-display-section.svelte b/web/src/lib/components/shared-components/search-bar/search-display-section.svelte index f3a4fa2b0c..00a5403068 100644 --- a/web/src/lib/components/shared-components/search-bar/search-display-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-display-section.svelte @@ -19,7 +19,7 @@ <div class="flex flex-wrap gap-x-5 gap-y-2 mt-1"> <Checkbox id="not-in-album-checkbox" label={$t('not_in_any_album')} bind:checked={filters.isNotInAlbum} /> <Checkbox id="archive-checkbox" label={$t('archive')} bind:checked={filters.isArchive} /> - <Checkbox id="favorite-checkbox" label={$t('favorite')} bind:checked={filters.isFavorite} /> + <Checkbox id="favorite-checkbox" label={$t('favorites')} bind:checked={filters.isFavorite} /> </div> </fieldset> </div> 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 f652f70fd8..0ede8d7fa1 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} <span><Icon path={mdiClose} ariaHidden /></span> - Collapse + {$t('collapse')} {:else} <span><Icon path={mdiArrowRight} ariaHidden /></span> - See all people + {$t('see_all_people')} {/if} </Button> </div> 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 07cd8e1b0e..68c5b2628c 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')} </button> {/if} </div> 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 4e33efd571..3def0ce08d 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')} </div> {/if} </div> 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 f6c1aa8139..502cd94cce 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')} </div> {/if} </div> 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 c3168dbb1c..5243a14931 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')} </div> {/if} </div> 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 d3da95ebe1..04bc72f3f6 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')} </div> {/if} </div> 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 99aa364455..b4efd90056 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')} </div> {/if} </div> 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 b2453fda27..d933b27ab5 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')} </div> {/if} </div> 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 038c7e81a1..2f579e9db4 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 @@ <script lang="ts"> import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; + import { t } from 'svelte-i18n'; export let value: string; export let label = ''; @@ -26,7 +27,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')} </div> {/if} </div> diff --git a/web/src/lib/components/shared-components/show-shortcuts.svelte b/web/src/lib/components/shared-components/show-shortcuts.svelte index 6cb2aa4524..9d79ed4648 100644 --- a/web/src/lib/components/shared-components/show-shortcuts.svelte +++ b/web/src/lib/components/shared-components/show-shortcuts.svelte @@ -19,7 +19,7 @@ const shortcuts: Shortcuts = { general: [ { key: ['←', '→'], action: $t('previous_or_next_photo') }, - { key: ['Esc'], action: 'Back, close, or deselect' }, + { key: ['Esc'], action: $t('back_close_deselect') }, { key: ['Ctrl', 'k'], action: $t('search_your_photos') }, { key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') }, ], @@ -30,7 +30,7 @@ { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') }, { key: ['⇧', 'd'], action: $t('download') }, { key: ['Space'], action: $t('play_or_pause_video') }, - { key: ['Del'], action: 'Trash/Delete Asset', info: 'press ⇧ to permanently delete asset' }, + { key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') }, ], }; const dispatch = createEventDispatcher<{ diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index 522251666b..cc291cdd91 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -8,7 +8,6 @@ import { uploadExecutionQueue } from '$lib/utils/file-uploader'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import { mdiCog, mdiWindowMinimize, mdiCancel, mdiCloudUploadOutline } from '@mdi/js'; - import { s } from '$lib/utils'; import { t } from 'svelte-i18n'; let showDetail = false; @@ -38,18 +37,18 @@ on:outroend={() => { if ($errorCounter > 0) { notificationController.show({ - message: `Upload completed with ${$errorCounter} error${s($errorCounter)}, refresh the page to see new upload assets.`, + message: $t('upload_errors', { values: { count: $errorCounter } }), type: NotificationType.Warning, }); } else if ($successCounter > 0) { notificationController.show({ - message: 'Upload success, refresh the page to see new upload assets.', + message: $t('upload_success'), type: NotificationType.Info, }); } if ($duplicateCounter > 0) { notificationController.show({ - message: `Skipped ${$duplicateCounter} duplicate asset${s($duplicateCounter)}`, + message: $t('upload_skipped_duplicates', { values: { count: $duplicateCounter } }), type: NotificationType.Warning, }); } @@ -65,12 +64,18 @@ <div class="place-item-center mb-4 flex justify-between"> <div class="flex flex-col gap-1"> <p class="immich-form-label text-xm"> - Remaining {$remainingUploads} - Processed {$successCounter + $errorCounter}/{$totalUploadCounter} + {$t('upload_progress', { + values: { + remaining: $remainingUploads, + processed: $successCounter + $errorCounter, + total: $totalUploadCounter, + }, + })} </p> <p class="immich-form-label text-xs"> - Uploaded <span class="text-immich-success">{$successCounter}</span> - Error - <span class="text-immich-error">{$errorCounter}</span> - - Duplicates <span class="text-immich-warning">{$duplicateCounter}</span> + {$t('upload_status_uploaded')} <span class="text-immich-success">{$successCounter}</span> - + {$t('upload_status_errors')} <span class="text-immich-error">{$errorCounter}</span> - + {$t('upload_status_duplicates')} <span class="text-immich-warning">{$duplicateCounter}</span> </p> </div> <div class="flex flex-col items-end"> diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte index ff161f1eeb..1ea541a968 100644 --- a/web/src/lib/components/shared-components/version-announcement-box.svelte +++ b/web/src/lib/components/shared-components/version-announcement-box.svelte @@ -35,7 +35,7 @@ </script> {#if showModal} - <FullScreenModal title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}> + <FullScreenModal title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)}> <div> <FormatMessage key="version_announcement_message" let:tag let:message> {#if tag === 'link'} @@ -53,9 +53,9 @@ <div class="mt-4 font-medium">Your friend, Alex</div> <div class="font-sm mt-8"> - <code>Server Version: {serverVersion}</code> + <code>{$t('server_version')}: {serverVersion}</code> <br /> - <code>Latest Version: {releaseVersion}</code> + <code>{$t('latest_version')}: {releaseVersion}</code> </div> <svelte:fragment slot="sticky-bottom"> 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 3d75df4259..06dad3913a 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 @@ <p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p> {:else} <p> - Expires {getCountDownExpirationDate()} + {$t('expires_date', { values: { date: getCountDownExpirationDate() } })} </p> {/if} {:else} - <p>Expires ∞</p> + <p>{$t('expires_date', { values: { date: '∞' } })}</p> {/if} </div> @@ -97,7 +97,7 @@ <div class="flex w-[80px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray" > - Upload + {$t('upload')} </div> {/if} @@ -105,7 +105,7 @@ <div class="flex w-[100px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray" > - Download + {$t('download')} </div> {/if} @@ -113,7 +113,7 @@ <div class="flex w-[60px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray" > - EXIF + {$t('exif').toUpperCase()} </div> {/if} @@ -121,7 +121,7 @@ <div class="flex w-[100px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray" > - Password + {$t('password')} </div> {/if} </div> 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 e6823aeed3..8e511abf4a 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 ee6026c52b..275f628f0a 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')); } }; </script> 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 40e517f410..88cda3cc0c 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} <p class="py-5 text-sm"> - Looks like you shared your photos with all users or you don't have any user to share with. + {$t('photo_shared_all_users')} </p> {/if} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index d46e9e4724..ee5cf8ed11 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. <link>Crontab Guru</link>", @@ -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 <link>{template}</link> 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 0cb93bf6cc..dfc6ab9127 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, string> = { - [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, string> = { + [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, string> = { + [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, string> = { - [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<AssetJobName, string> = { @@ -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 = <T>(promise: Promise<T>): 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 <T>(fn: () => Promise<T>): Promise<[undefined, T] | [unknown, undefined]> => { try { diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index ecfd29a8fc..75232e793d 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 cc0c3cbdc9..aff76ef88e 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 3bacab5915..da80f3c3b8 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 ? `<b>${encodeHTMLSpecialChars(albumName)}</b>` : '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<DownloadIn const [error, downloadInfo] = await withError(() => 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<DownloadIn downloadBlob(data, archiveName); } catch (error) { - handleError(error, 'Unable to download files'); + const $t = get(t); + handleError(error, $t('errors.unable_to_download_files')); downloadManager.clear(downloadKey); return; } finally { @@ -144,10 +148,11 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn }; export const downloadFile = async (asset: AssetResponseDto) => { + 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<AssetResponseDto>, 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 018dbfbb87..06cddc9bdf 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 e73a0584a6..d581a440a8 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 @@ <svelte:fragment slot="leading"> <p class="text-lg dark:text-immich-dark-fg"> {#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} </p> </svelte:fragment> @@ -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')} </button> <Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} on:click={handleAddAssets} >{$t('done')}</Button diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 29c2a50ae5..432410798f 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -161,21 +161,16 @@ if (results.length - count > 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 @@ <div class="flex flex-col content-center items-center text-center"> <Icon path={mdiAccountOff} size="3.5em" /> <p class="mt-5 text-3xl font-medium max-w-lg line-clamp-2 overflow-hidden"> - {`No people${searchName ? ` named "${searchName}"` : ''}`} + {$t(searchName ? 'search_no_people_named' : 'search_no_people', { values: { name: searchName } })} </p> </div> </div> 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 d239da64c3..eecfbf29b2 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} <p class="w-40 sm:w-72 font-medium truncate">{data.person.name}</p> <p class="absolute w-fit text-sm text-gray-500 dark:text-immich-gray bottom-0"> - {`${numberOfAssets} asset${s(numberOfAssets)}`} + {$t('assets_count', { values: { count: numberOfAssets } })} </p> {:else} <p class="font-medium">{$t('add_a_name')}</p> 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 7723a86b41..9eb7d76546 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 @@ <div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">{$t('albums').toUpperCase()}</div> <AlbumCardGroup albums={searchResultAlbums} showDateRange showItemCount /> - <div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">PHOTOS & VIDEOS</div> + <div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80"> + {$t('photos_and_videos').toUpperCase()} + </div> </section> {/if} <section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg"> @@ -296,7 +298,7 @@ <div class="flex flex-col content-center items-center text-center"> <Icon path={mdiImageOffOutline} size="3.5em" /> <p class="mt-5 text-3xl font-medium">{$t('no_results')}</p> - <p class="text-base font-normal">Try a synonym or more general keyword</p> + <p class="text-base font-normal">{$t('no_results_description')}</p> </div> </div> {/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 baef91b39d..40dd3b5d31 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 3e0ab8bb60..dc614d0f0e 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 e2a809c6d5..e82605d83e 100644 --- a/web/src/routes/+error.svelte +++ b/web/src/routes/+error.svelte @@ -35,7 +35,7 @@ <div> <div class="flex items-center justify-between gap-4 px-4 py-4"> <h1 class="font-medium text-immich-primary dark:text-immich-dark-primary"> - 🚨 Error - Something went wrong + 🚨 {$t('error_title')} </h1> <div class="flex justify-end"> <CircleIconButton diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index 6f9bcf000f..aa23e4e7d2 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -6,6 +6,7 @@ import { resetSavedUser, user } from '$lib/stores/user.store'; import { logout } from '@immich/sdk'; import type { PageData } from './$types'; + import { t } from 'svelte-i18n'; export let data: PageData; @@ -18,11 +19,10 @@ <FullscreenContainer title={data.meta.title}> <p slot="message"> - Hi {$user.name} ({$user.email}), + {$t('hi_user', { values: { name: $user.name, email: $user.email } })} <br /> <br /> - 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')} </p> <ChangePasswordForm on:success={onSuccess} /> diff --git a/web/src/routes/auth/change-password/+page.ts b/web/src/routes/auth/change-password/+page.ts index 4c9fc0cb7e..0e961d253c 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 e308e117f7..397b945661 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 4eb54e6095..c578acc2e7 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 c5db69f043..9c1f0ca6c4 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; </script> <FullscreenContainer title={data.meta.title}> <p slot="message"> - 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')} </p> <AdminRegistrationForm /> diff --git a/web/src/routes/auth/register/+page.ts b/web/src/routes/auth/register/+page.ts index cda5d9d3f8..9ecf1fda96 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;