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>
This commit is contained in:
waclaw66 2024-06-24 15:50:01 +02:00 committed by GitHub
parent df9e074304
commit dd2c7400a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 635 additions and 322 deletions

View File

@ -160,7 +160,7 @@
{ value: '1080', text: '1080p' }, { value: '1080', text: '1080p' },
{ value: '720', text: '720p' }, { value: '720', text: '720p' },
{ value: '480', text: '480p' }, { value: '480', text: '480p' },
{ value: 'original', text: 'original' }, { value: 'original', text: $t('original') },
]} ]}
name="resolution" name="resolution"
isEdited={config.ffmpeg.targetResolution !== savedConfig.ffmpeg.targetResolution} isEdited={config.ffmpeg.targetResolution !== savedConfig.ffmpeg.targetResolution}
@ -191,7 +191,7 @@
bind:value={config.ffmpeg.transcode} bind:value={config.ffmpeg.transcode}
name="transcode" name="transcode"
options={[ options={[
{ value: TranscodePolicy.All, text: 'All videos' }, { value: TranscodePolicy.All, text: $t('all_videos') },
{ {
value: TranscodePolicy.Optimal, value: TranscodePolicy.Optimal,
text: $t('admin.transcoding_optimal_description'), text: $t('admin.transcoding_optimal_description'),
@ -233,7 +233,7 @@
}, },
{ {
value: ToneMapping.Disabled, value: ToneMapping.Disabled,
text: 'Disabled', text: $t('disabled'),
}, },
]} ]}
isEdited={config.ffmpeg.tonemap !== savedConfig.ffmpeg.tonemap} isEdited={config.ffmpeg.tonemap !== savedConfig.ffmpeg.tonemap}

View File

@ -2,6 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { albumFactory } from '@test-data'; import { albumFactory } from '@test-data';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte'; import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
import { init, register, waitLocale } from 'svelte-i18n';
import AlbumCard from '../album-card.svelte'; import AlbumCard from '../album-card.svelte';
const onShowContextMenu = vi.fn(); const onShowContextMenu = vi.fn();
@ -9,6 +10,12 @@ const onShowContextMenu = vi.fn();
describe('AlbumCard component', () => { describe('AlbumCard component', () => {
let sut: RenderResult<AlbumCard>; 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([ it.each([
{ {
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }), album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
@ -36,7 +43,7 @@ describe('AlbumCard component', () => {
const albumImgElement = sut.getByTestId('album-image'); const albumImgElement = sut.getByTestId('album-image');
const albumNameElement = sut.getByTestId('album-name'); const albumNameElement = sut.getByTestId('album-name');
const albumDetailsElement = sut.getByTestId('album-details'); 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('src');
expect(albumImgElement).toHaveAttribute('alt', album.albumName); expect(albumImgElement).toHaveAttribute('alt', album.albumName);

View File

@ -9,6 +9,7 @@
import { mdiChevronRight } from '@mdi/js'; import { mdiChevronRight } from '@mdi/js';
import AlbumCard from '$lib/components/album-page/album-card.svelte'; import AlbumCard from '$lib/components/album-page/album-card.svelte';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { t } from 'svelte-i18n';
export let albums: AlbumResponseDto[]; export let albums: AlbumResponseDto[];
export let group: AlbumGroup | undefined = undefined; export let group: AlbumGroup | undefined = undefined;
@ -41,7 +42,7 @@
class="inline-block -mt-2.5 transition-all duration-[250ms] {iconRotation}" 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="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> </button>
<hr class="dark:border-immich-dark-gray" /> <hr class="dark:border-immich-dark-gray" />
</div> </div>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { locale } from '$lib/stores/preferences.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import type { AlbumResponseDto } from '@immich/sdk'; import type { AlbumResponseDto } from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
@ -7,7 +6,6 @@
import { getShortDateRange } from '$lib/utils/date-time'; import { getShortDateRange } from '$lib/utils/date-time';
import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { s } from '$lib/utils';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
@ -66,8 +64,7 @@
<span class="flex gap-2 text-sm dark:text-immich-dark-fg" data-testid="album-details"> <span class="flex gap-2 text-sm dark:text-immich-dark-fg" data-testid="album-details">
{#if showItemCount} {#if showItemCount}
<p> <p>
{album.assetCount.toLocaleString($locale)} {$t('items_count', { values: { count: album.assetCount } })}
item{s(album.assetCount)}
</p> </p>
{/if} {/if}
@ -79,7 +76,7 @@
{#if $user.id === album.ownerId} {#if $user.id === album.ownerId}
<p>{$t('owned')}</p> <p>{$t('owned')}</p>
{:else if album.owner} {:else if album.owner}
<p>Shared by {album.owner.name}</p> <p>{$t('shared_by_user', { values: { user: album.owner.name } })}</p>
{:else} {:else}
<p>{$t('shared')}</p> <p>{$t('shared')}</p>
{/if} {/if}

View File

@ -65,7 +65,7 @@
/> />
{/if} {/if}
<SettingSwitch <SettingSwitch
title="Comments & likes" title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')} subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled} checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')} on:toggle={() => dispatch('toggleEnableActivity')}

View File

@ -2,6 +2,7 @@
import { dateFormats } from '$lib/constants'; import { dateFormats } from '$lib/constants';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import type { AlbumResponseDto } from '@immich/sdk'; import type { AlbumResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto; 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 class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
<span>{getDateRange(startDate, endDate)}</span> <span>{getDateRange(startDate, endDate)}</span>
<span></span> <span></span>
<span>{album.assetCount} items</span> <span>{$t('items_count', { values: { count: album.assetCount } })}</span>
</span> </span>

View File

@ -4,6 +4,7 @@
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { import {
AlbumFilter, AlbumFilter,
AlbumSortBy,
AlbumGroupBy, AlbumGroupBy,
AlbumViewMode, AlbumViewMode,
albumViewSettings, albumViewSettings,
@ -25,6 +26,7 @@
type AlbumGroupOptionMetadata, type AlbumGroupOptionMetadata,
type AlbumSortOptionMetadata, type AlbumSortOptionMetadata,
findGroupOptionMetadata, findGroupOptionMetadata,
findFilterOption,
findSortOptionMetadata, findSortOptionMetadata,
getSelectedAlbumGroupOption, getSelectedAlbumGroupOption,
groupOptionsMetadata, groupOptionsMetadata,
@ -43,6 +45,11 @@
return ordering === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc; 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) => { const handleChangeGroupBy = ({ id, defaultOrder }: AlbumGroupOptionMetadata) => {
if ($albumViewSettings.groupBy === id) { if ($albumViewSettings.groupBy === id) {
$albumViewSettings.groupOrder = flipOrdering($albumViewSettings.groupOrder); $albumViewSettings.groupOrder = flipOrdering($albumViewSettings.groupOrder);
@ -69,6 +76,10 @@
let selectedGroupOption: AlbumGroupOptionMetadata; let selectedGroupOption: AlbumGroupOptionMetadata;
let groupIcon: string; let groupIcon: string;
$: selectedFilterOption = albumFilterNames[findFilterOption($albumViewSettings.filter)];
$: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy);
$: { $: {
selectedGroupOption = findGroupOptionMetadata($albumViewSettings.groupBy); selectedGroupOption = findGroupOptionMetadata($albumViewSettings.groupBy);
if (selectedGroupOption.isDisabled()) { if (selectedGroupOption.isDisabled()) {
@ -76,8 +87,6 @@
} }
} }
$: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy);
$: { $: {
if (selectedGroupOption.id === AlbumGroupBy.None) { if (selectedGroupOption.id === AlbumGroupBy.None) {
groupIcon = mdiFolderRemoveOutline; groupIcon = mdiFolderRemoveOutline;
@ -88,14 +97,41 @@
} }
$: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin; $: 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> </script>
<!-- Filter Albums by Sharing Status (All, Owned, Shared) --> <!-- Filter Albums by Sharing Status (All, Owned, Shared) -->
<div class="hidden xl:block h-10"> <div class="hidden xl:block h-10">
<GroupTab <GroupTab
filters={Object.keys(AlbumFilter)} filters={Object.values(albumFilterNames)}
selected={$albumViewSettings.filter} selected={selectedFilterOption}
onSelect={(selected) => ($albumViewSettings.filter = selected)} onSelect={(selected) => handleChangeAlbumFilter(selected, AlbumFilter.All)}
/> />
</div> </div>
@ -118,8 +154,8 @@
options={Object.values(sortOptionsMetadata)} options={Object.values(sortOptionsMetadata)}
selectedOption={selectedSortOption} selectedOption={selectedSortOption}
on:select={({ detail }) => handleChangeSortBy(detail)} on:select={({ detail }) => handleChangeSortBy(detail)}
render={({ text }) => ({ render={({ id }) => ({
title: text, title: albumSortByNames[id],
icon: sortIcon, icon: sortIcon,
})} })}
/> />
@ -130,8 +166,8 @@
options={Object.values(groupOptionsMetadata)} options={Object.values(groupOptionsMetadata)}
selectedOption={selectedGroupOption} selectedOption={selectedGroupOption}
on:select={({ detail }) => handleChangeGroupBy(detail)} on:select={({ detail }) => handleChangeGroupBy(detail)}
render={({ text, isDisabled }) => ({ render={({ id, isDisabled }) => ({
title: text, title: albumGroupByNames[id],
icon: groupIcon, icon: groupIcon,
disabled: isDisabled(), disabled: isDisabled(),
})} })}

View File

@ -304,7 +304,7 @@
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'delete-album', 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) { if (!isConfirmed) {
@ -340,7 +340,7 @@
message: $t('album_info_updated'), message: $t('album_info_updated'),
type: NotificationType.Info, type: NotificationType.Info,
button: { button: {
text: 'View Album', text: $t('view_album'),
onClick() { onClick() {
return goto(`${AppRoute.ALBUMS}/${album.id}`); return goto(`${AppRoute.ALBUMS}/${album.id}`);
}, },

View File

@ -1,6 +1,7 @@
<script lang="ts"> <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 type { AlbumSortOptionMetadata } from '$lib/utils/album-utils';
import { t } from 'svelte-i18n';
export let option: AlbumSortOptionMetadata; export let option: AlbumSortOptionMetadata;
@ -12,6 +13,17 @@
$albumViewSettings.sortOrder = option.defaultOrder; $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> </script>
<th class="text-sm font-medium {option.columnStyle}"> <th class="text-sm font-medium {option.columnStyle}">
@ -27,6 +39,6 @@
&#8593; &#8593;
{/if} {/if}
{/if} {/if}
{option.text} {albumSortByNames[option.id]}
</button> </button>
</th> </th>

View File

@ -34,7 +34,9 @@
path={mdiShareVariantOutline} path={mdiShareVariantOutline}
size="16" size="16"
class="inline ml-1 opacity-70" 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} {/if}
</td> </td>

View File

@ -13,6 +13,7 @@
sortOptionsMetadata, sortOptionsMetadata,
type AlbumGroup, type AlbumGroup,
} from '$lib/utils/album-utils'; } from '$lib/utils/album-utils';
import { t } from 'svelte-i18n';
export let groupedAlbums: AlbumGroup[]; export let groupedAlbums: AlbumGroup[];
export let albumGroupOption: string = AlbumGroupBy.None; export let albumGroupOption: string = AlbumGroupBy.None;
@ -58,8 +59,7 @@
/> />
<span class="font-bold text-2xl">{albumGroup.name}</span> <span class="font-bold text-2xl">{albumGroup.name}</span>
<span class="ml-1.5"> <span class="ml-1.5">
({albumGroup.albums.length} ({$t('albums_count', { values: { count: albumGroup.albums.length } })})
{albumGroup.albums.length > 1 ? 'albums' : 'album'})
</span> </span>
</td> </td>
</tr> </tr>

View File

@ -53,7 +53,10 @@
try { try {
await removeUserFromAlbum({ id: album.id, userId }); await removeUserFromAlbum({ id: album.id, userId });
dispatch('remove', 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 }); notificationController.show({ type: NotificationType.Info, message });
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_remove_album_users')); handleError(error, $t('errors.unable_to_remove_album_users'));
@ -65,7 +68,9 @@
const handleSetReadonly = async (user: UserResponseDto, role: AlbumUserRole) => { const handleSetReadonly = async (user: UserResponseDto, role: AlbumUserRole) => {
try { try {
await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } }); 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'); dispatch('refreshAlbum');
notificationController.show({ type: NotificationType.Info, message }); notificationController.show({ type: NotificationType.Info, message });
} catch (error) { } catch (error) {
@ -101,9 +106,9 @@
<div id="icon-{user.id}" class="flex place-items-center gap-2 text-sm"> <div id="icon-{user.id}" class="flex place-items-center gap-2 text-sm">
<div> <div>
{#if role === AlbumUserRole.Viewer} {#if role === AlbumUserRole.Viewer}
Viewer {$t('role_viewer')}
{:else} {:else}
Editor {$t('role_editor')}
{/if} {/if}
</div> </div>
{#if isOwned} {#if isOwned}
@ -135,8 +140,8 @@
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id} {#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}
<ConfirmDialog <ConfirmDialog
title="Leave album?" title={$t('album_leave')}
prompt="Are you sure you want to leave {album.albumName}?" prompt={$t('album_leave_confirmation', { values: { album: album.albumName } })}
confirmText={$t('leave')} confirmText={$t('leave')}
onConfirm={handleRemoveUser} onConfirm={handleRemoveUser}
onCancel={() => (selectedRemoveUser = null)} onCancel={() => (selectedRemoveUser = null)}
@ -145,9 +150,9 @@
{#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id} {#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id}
<ConfirmDialog <ConfirmDialog
title="Remove user?" title={$t('album_remove_user')}
prompt="Are you sure you want to remove {selectedRemoveUser.name}?" prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
confirmText={$t('remove')} confirmText={$t('remove_user')}
onConfirm={handleRemoveUser} onConfirm={handleRemoveUser}
onCancel={() => (selectedRemoveUser = null)} onCancel={() => (selectedRemoveUser = null)}
/> />

View File

@ -37,7 +37,7 @@
disabled={selectedThumbnail == undefined} disabled={selectedThumbnail == undefined}
on:click={() => dispatch('thumbnail', selectedThumbnail)} on:click={() => dispatch('thumbnail', selectedThumbnail)}
> >
Done {$t('done')}
</Button> </Button>
</svelte:fragment> </svelte:fragment>
</ControlAppBar> </ControlAppBar>

View File

@ -24,9 +24,9 @@
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {}; let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [ const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
{ title: $t('editor'), value: AlbumUserRole.Editor, icon: mdiPencil }, { title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
{ title: $t('viewer'), value: AlbumUserRole.Viewer, icon: mdiEye }, { title: $t('role_viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
{ title: $t('remove'), value: 'none' }, { title: $t('remove_user'), value: 'none' },
]; ];
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
@ -110,7 +110,7 @@
{#if users.length + Object.keys(selectedUsers).length === 0} {#if users.length + Object.keys(selectedUsers).length === 0}
<p class="p-5 text-sm"> <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> </p>
{/if} {/if}

View File

@ -8,6 +8,7 @@
import { isTenMinutesApart } from '$lib/utils/timesince'; import { isTenMinutesApart } from '$lib/utils/timesince';
import { import {
ReactionType, ReactionType,
Type,
createActivity, createActivity,
deleteActivity, deleteActivity,
getActivities, getActivities,
@ -41,7 +42,7 @@
const diff = dateTime.diffNow().shiftTo(...units); const diff = dateTime.diffNow().shiftTo(...units);
const unit = units.find((unit) => diff.get(unit) !== 0) || 'second'; const unit = units.find((unit) => diff.get(unit) !== 0) || 'second';
const relativeFormatter = new Intl.RelativeTimeFormat('en', { const relativeFormatter = new Intl.RelativeTimeFormat($locale, {
numeric: 'auto', numeric: 'auto',
}); });
return relativeFormatter.format(Math.trunc(diff.as(unit)), unit); return relativeFormatter.format(Math.trunc(diff.as(unit)), unit);
@ -115,8 +116,13 @@
} else { } else {
dispatch('deleteComment'); dispatch('deleteComment');
} }
const deleteMessages: Record<Type, string> = {
[Type.Comment]: $t('comment_deleted'),
[Type.Like]: $t('like_deleted'),
};
notificationController.show({ notificationController.show({
message: `${reaction.type} deleted`, message: deleteMessages[reaction.type],
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
@ -216,7 +222,12 @@
<div class="text-red-600"><Icon path={mdiHeart} size={20} /></div> <div class="text-red-600"><Icon path={mdiHeart} size={20} /></div>
<div class="w-full" title={`${reaction.user.name} (${reaction.user.email})`}> <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> </div>
{#if assetId === undefined && reaction.assetId} {#if assetId === undefined && reaction.assetId}
<a <a

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { AlbumResponseDto } from '@immich/sdk'; import type { AlbumResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
</script> </script>
<span>{album.assetCount} items</span> <span>{$t('items_count', { values: { count: album.assetCount } })}</span>
{#if album.shared} {#if album.shared}
<span>• Shared</span> <span>• {$t('shared')}</span>
{/if} {/if}

View File

@ -225,18 +225,18 @@
<MenuOption <MenuOption
icon={mdiDatabaseRefreshOutline} icon={mdiDatabaseRefreshOutline}
onClick={() => onJobClick(AssetJobName.RefreshMetadata)} onClick={() => onJobClick(AssetJobName.RefreshMetadata)}
text={getAssetJobName(AssetJobName.RefreshMetadata)} text={$getAssetJobName(AssetJobName.RefreshMetadata)}
/> />
<MenuOption <MenuOption
icon={mdiImageRefreshOutline} icon={mdiImageRefreshOutline}
onClick={() => onJobClick(AssetJobName.RegenerateThumbnail)} onClick={() => onJobClick(AssetJobName.RegenerateThumbnail)}
text={getAssetJobName(AssetJobName.RegenerateThumbnail)} text={$getAssetJobName(AssetJobName.RegenerateThumbnail)}
/> />
{#if asset.type === AssetTypeEnum.Video} {#if asset.type === AssetTypeEnum.Video}
<MenuOption <MenuOption
icon={mdiCogRefreshOutline} icon={mdiCogRefreshOutline}
onClick={() => onJobClick(AssetJobName.TranscodeVideo)} onClick={() => onJobClick(AssetJobName.TranscodeVideo)}
text={getAssetJobName(AssetJobName.TranscodeVideo)} text={$getAssetJobName(AssetJobName.TranscodeVideo)}
/> />
{/if} {/if}
{/if} {/if}

View File

@ -162,7 +162,7 @@
reactions = [...reactions, isLiked]; reactions = [...reactions, isLiked];
} }
} catch (error) { } 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 }); const { comments } = await getActivityStatistics({ assetId: asset.id, albumId: album.id });
numberOfComments = comments; numberOfComments = comments;
} catch (error) { } 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({ notificationController.show({
type: NotificationType.Info, 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) { } 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({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Restored asset`, message: $t('restored_asset'),
}); });
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_restore_assets')); handleError(error, $t('errors.unable_to_restore_assets'));
@ -446,9 +446,9 @@
const handleRunJob = async (name: AssetJobName) => { const handleRunJob = async (name: AssetJobName) => {
try { try {
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } }); 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) { } catch (error) {
handleError(error, `Unable to submit job`); handleError(error, $t('errors.unable_to_submit_job'));
} }
}; };
@ -528,7 +528,7 @@
timeout: 1500, timeout: 1500,
}); });
} catch (error) { } catch (error) {
handleError(error, 'Unable to update album cover'); handleError(error, $t('errors.unable_to_update_album_cover'));
} }
}; };

View File

@ -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-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"> <div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
<p> <p>
This asset is offline. Immich can not access its file location. Please ensure the asset is available and {$t('asset_offline_description')}
then rescan the library.
</p> </p>
</div> </div>
</div> </div>
@ -170,8 +169,8 @@
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
{#if unassignedFaces.length > 0} {#if unassignedFaces.length > 0}
<Icon <Icon
ariaLabel="Asset has unassigned faces" ariaLabel={$t('asset_has_unassigned_faces')}
title="Asset has unassigned faces" title={$t('asset_has_unassigned_faces')}
color="currentColor" color="currentColor"
path={mdiAccountOff} path={mdiAccountOff}
size="24" size="24"
@ -243,11 +242,11 @@
)} )}
> >
{#if ageInMonths <= 11} {#if ageInMonths <= 11}
Age {ageInMonths} months {$t('age_months', { values: { months: ageInMonths } })}
{:else if ageInMonths > 12 && ageInMonths <= 23} {:else if ageInMonths > 12 && ageInMonths <= 23}
Age 1 year, {ageInMonths - 12} months {$t('age_year_months', { values: { months: ageInMonths - 12 } })}
{:else} {:else}
Age {age} {$t('age_years', { values: { years: age } })}
{/if} {/if}
</p> </p>
{/if} {/if}
@ -452,7 +451,7 @@
target="_blank" target="_blank"
class="font-medium text-immich-primary" class="font-medium text-immich-primary"
> >
Open in OpenStreetMap {$t('open_in_openstreetmap')}
</a> </a>
</div> </div>
</svelte:fragment> </svelte:fragment>

View File

@ -82,7 +82,7 @@
const mergedPerson = await getPerson({ id: person.id }); const mergedPerson = await getPerson({ id: person.id });
const count = results.filter(({ success }) => success).length; const count = results.filter(({ success }) => success).length;
notificationController.show({ notificationController.show({
message: `Merged ${count} ${count === 1 ? 'person' : 'people'}`, message: $t('merged_people_count', { values: { count: count } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
dispatch('merge', mergedPerson); dispatch('merge', mergedPerson);
@ -101,7 +101,7 @@
<ControlAppBar on:close={onClose}> <ControlAppBar on:close={onClose}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
{#if hasSelection} {#if hasSelection}
{$t('selected')} {selectedPeople.length} {$t('selected_count', { values: { count: selectedPeople.length } })}
{:else} {:else}
{$t('merge_people')} {$t('merge_people')}
{/if} {/if}

View File

@ -99,10 +99,10 @@
</div> </div>
<div class="flex px-4 md:pt-4"> <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>
<div class="flex px-4 pt-2"> <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> </div>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button> <Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button>

View File

@ -62,7 +62,7 @@
searchedPeople = data; searchedPeople = data;
searchWord = searchName; searchWord = searchName;
} catch (error) { } catch (error) {
handleError(error, $t('cant_search_people')); handleError(error, $t('errors.cant_search_people'));
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
timeout = null; timeout = null;

View File

@ -68,7 +68,7 @@
allPeople = people; allPeople = people;
peopleWithFaces = await getFaces({ id: assetId }); peopleWithFaces = await getFaces({ id: assetId });
} catch (error) { } catch (error) {
handleError(error, $t('cant_get_faces')); handleError(error, $t('errors.cant_get_faces'));
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }
@ -142,11 +142,11 @@
} }
notificationController.show({ notificationController.show({
message: `Edited ${numberOfChanges} ${numberOfChanges > 1 ? 'people' : 'person'}`, message: $t('people_edits_count', { values: { count: numberOfChanges } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } 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" class="justify-self-end rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
on:click={() => handleEditFaces()} on:click={() => handleEditFaces()}
> >
Done {$t('done')}
</button> </button>
{:else} {:else}
<LoadingSpinner /> <LoadingSpinner />
@ -299,7 +299,7 @@
<CircleIconButton <CircleIconButton
color="primary" color="primary"
icon={mdiRestart} icon={mdiRestart}
title="Reset" title={$t('reset')}
size="18" size="18"
padding="1" padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform" class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"

View File

@ -24,7 +24,7 @@
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}> <FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}>
<div class="text-immich-primary dark:text-immich-dark-primary"> <div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg"> <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> </p>
</div> </div>

View File

@ -19,7 +19,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import FaceThumbnail from './face-thumbnail.svelte'; import FaceThumbnail from './face-thumbnail.svelte';
import PeopleList from './people-list.svelte'; import PeopleList from './people-list.svelte';
import { s } from '$lib/utils'; import { t } from 'svelte-i18n';
export let assetIds: string[]; export let assetIds: string[];
export let personAssets: PersonResponseDto; export let personAssets: PersonResponseDto;
@ -77,11 +77,11 @@
await reassignFaces({ id: data.id, assetFaceUpdateDto: { data: selectedPeople } }); await reassignFaces({ id: data.id, assetFaceUpdateDto: { data: selectedPeople } });
notificationController.show({ 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, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
handleError(error, 'Unable to reassign assets to a new person'); handleError(error, $t('errors.unable_to_reassign_assets_new_person'));
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }
@ -97,14 +97,17 @@
if (selectedPerson) { if (selectedPerson) {
await reassignFaces({ id: selectedPerson.id, assetFaceUpdateDto: { data: selectedPeople } }); await reassignFaces({ id: selectedPerson.id, assetFaceUpdateDto: { data: selectedPeople } });
notificationController.show({ notificationController.show({
message: `Re-assigned ${assetIds.length} asset${s(assetIds.length)} to ${ message: $t('reassigned_assets_to_existing_person', {
selectedPerson.name || 'an existing person' values: { count: assetIds.length, name: selectedPerson.name || null },
}`, }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} }
} catch (error) { } 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 { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }
@ -128,7 +131,7 @@
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
<div class="flex gap-4"> <div class="flex gap-4">
<Button <Button
title={'Assign selected assets to a new person'} title={$t('create_new_person_hint')}
size={'sm'} size={'sm'}
disabled={disableButtons || hasSelection} disabled={disableButtons || hasSelection}
on:click={handleCreate} on:click={handleCreate}
@ -138,11 +141,11 @@
{:else} {:else}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
<span class="ml-2"> Create new Person</span></Button <span class="ml-2"> {$t('create_new_person')}</span></Button
> >
<Button <Button
size={'sm'} size={'sm'}
title={'Assign selected assets to an existing person'} title={$t('reassing_hint')}
disabled={disableButtons || !hasSelection} disabled={disableButtons || !hasSelection}
on:click={handleReassign} on:click={handleReassign}
> >
@ -153,7 +156,7 @@
{:else} {:else}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
<span class="ml-2"> Reassign</span></Button <span class="ml-2"> {$t('reassign')}</span></Button
> >
</div> </div>
</svelte:fragment> </svelte:fragment>

View File

@ -33,7 +33,7 @@
await signUpAdmin({ signUpDto: { email, password, name } }); await signUpAdmin({ signUpDto: { email, password, name } });
await goto(AppRoute.AUTH_LOGIN); await goto(AppRoute.AUTH_LOGIN);
} catch (error) { } 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'); errorMessage = $t('errors.unable_to_create_admin_account');
} }
} }

View File

@ -22,7 +22,7 @@
dispatch('submit', apiKey); dispatch('submit', apiKey);
} else { } else {
notificationController.show({ notificationController.show({
message: "Your API Key name shouldn't be empty", message: $t('api_key_empty'),
type: NotificationType.Warning, type: NotificationType.Warning,
}); });
} }

View File

@ -17,7 +17,7 @@
<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}> <FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}>
<div class="text-immich-primary dark:text-immich-dark-primary"> <div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg"> <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> </p>
</div> </div>

View File

@ -57,6 +57,6 @@
<p class="text-sm text-immich-primary">{success}</p> <p class="text-sm text-immich-primary">{success}</p>
{/if} {/if}
<div class="my-5 flex w-full"> <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> </div>
</form> </form>

View File

@ -30,7 +30,7 @@
album.description = description; album.description = description;
onEditSuccess?.(album); onEditSuccess?.(album);
} catch (error) { } catch (error) {
handleError(error, 'Unable to update album info'); handleError(error, $t('errors.unable_to_update_album_info'));
} finally { } finally {
isSubmitting = false; isSubmitting = false;
} }

View File

@ -36,7 +36,7 @@
return; return;
} catch (error) { } catch (error) {
console.error('Error [login-form] [oauth.callback]', 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; oauthLoading = false;
} }
} }
@ -48,7 +48,7 @@
return; return;
} }
} catch (error) { } catch (error) {
handleError(error, 'Unable to connect!'); handleError(error, $t('errors.unable_to_connect'));
} }
oauthLoading = false; oauthLoading = false;
@ -74,7 +74,7 @@
await onSuccess(); await onSuccess();
return; return;
} catch (error) { } catch (error) {
errorMessage = getServerErrorMessage(error) || 'Incorrect email or password'; errorMessage = getServerErrorMessage(error) || $t('errors.incorrect_email_or_password');
loading = false; loading = false;
return; return;
} }
@ -86,7 +86,7 @@
const success = await oauth.authorize(window.location); const success = await oauth.authorize(window.location);
if (!success) { if (!success) {
oauthLoading = false; oauthLoading = false;
oauthError = 'Unable to login with OAuth'; oauthError = $t('errors.unable_to_login_with_oauth');
} }
}; };
</script> </script>
@ -124,7 +124,7 @@
<LoadingSpinner /> <LoadingSpinner />
</span> </span>
{:else} {:else}
Login {$t('to_login')}
{/if} {/if}
</Button> </Button>
</div> </div>
@ -138,7 +138,7 @@
<span <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" 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> </span>
</div> </div>
{/if} {/if}

View File

@ -57,7 +57,7 @@
settings.dateBefore = ''; settings.dateBefore = '';
}} }}
> >
Remove custom date range {$t('remove_custom_date_range')}
</LinkButton> </LinkButton>
</div> </div>
</div> </div>
@ -70,7 +70,7 @@
options={[ options={[
{ {
value: '', value: '',
text: 'All', text: $t('all'),
}, },
{ {
value: Duration.fromObject({ hours: 24 }).toISO() || '', value: Duration.fromObject({ hours: 24 }).toISO() || '',
@ -101,7 +101,7 @@
settings.relativeDate = ''; settings.relativeDate = '';
}} }}
> >
Use custom date range instead {$t('use_custom_date_range')}
</LinkButton> </LinkButton>
</div> </div>
</div> </div>

View File

@ -16,9 +16,9 @@
<OnboardingCard> <OnboardingCard>
<ImmichLogo noText width="75" /> <ImmichLogo noText width="75" />
<p class="font-medium text-6xl my-6 text-immich-primary dark:text-immich-dark-primary"> <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>
<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"> <div class="w-full flex place-content-end">
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}> <Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}>

View File

@ -61,7 +61,7 @@
}} }}
> >
<span class="flex place-content-center place-items-center gap-2"> <span class="flex place-content-center place-items-center gap-2">
Done {$t('done')}
<Icon path={mdiCheck} size="18" /> <Icon path={mdiCheck} size="18" />
</span> </span>
</Button> </Button>

View File

@ -19,7 +19,7 @@
<p class="text-xl text-immich-primary dark:text-immich-dark-primary">{$t('color_theme').toUpperCase()}</p> <p class="text-xl text-immich-primary dark:text-immich-dark-primary">{$t('color_theme').toUpperCase()}</p>
<div> <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>
<div class="flex gap-4 mb-6"> <div class="flex gap-4 mb-6">

View File

@ -24,7 +24,7 @@
try { try {
const ids = [...getOwnedAssets()].map(({ id }) => id); const ids = [...getOwnedAssets()].map(({ id }) => id);
await runAssetJobs({ assetJobsDto: { assetIds: ids, name } }); await runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
notificationController.show({ message: getAssetJobMessage(name), type: NotificationType.Info }); notificationController.show({ message: $getAssetJobMessage(name), type: NotificationType.Info });
clearSelect(); clearSelect();
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_submit_job')); handleError(error, $t('errors.unable_to_submit_job'));
@ -34,6 +34,6 @@
{#each jobs as job} {#each jobs as job}
{#if isAllVideos || job !== AssetJobName.TranscodeVideo} {#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} {/if}
{/each} {/each}

View File

@ -44,13 +44,15 @@
onFavorite(ids, isFavorite); onFavorite(ids, isFavorite);
notificationController.show({ 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, type: NotificationType.Info,
}); });
clearSelect(); clearSelect();
} catch (error) { } 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 { } finally {
loading = false; loading = false;
} }

View File

@ -8,7 +8,6 @@
import { mdiDeleteOutline, mdiImageRemoveOutline } from '@mdi/js'; import { mdiDeleteOutline, mdiImageRemoveOutline } from '@mdi/js';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { s } from '$lib/utils';
import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -21,7 +20,7 @@
const removeFromAlbum = async () => { const removeFromAlbum = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'remove-from-album', 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) { if (!isConfirmed) {
@ -42,7 +41,7 @@
const count = results.filter(({ success }) => success).length; const count = results.filter(({ success }) => success).length;
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Removed ${count} asset${s(count)}`, message: $t('assets_removed_count', { values: { count: count } }),
}); });
clearSelect(); clearSelect();
@ -50,7 +49,7 @@
console.error('Error [album-viewer] [removeAssetFromAlbum]', error); console.error('Error [album-viewer] [removeAssetFromAlbum]', error);
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: 'Error removing assets from album, check console for more details', message: $t('errors.error_removing_assets_from_album'),
}); });
} }
}; };

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; 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 { handleError } from '$lib/utils/handle-error';
import { removeSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk'; import { removeSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiDeleteOutline } from '@mdi/js'; import { mdiDeleteOutline } from '@mdi/js';
@ -16,9 +16,9 @@
const handleRemove = async () => { const handleRemove = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'remove-from-shared-link', id: 'remove-from-shared-link',
title: 'Remove assets?', title: $t('remove_assets_title'),
prompt: `Are you sure you want to remove ${getAssets().size} asset${s(getAssets().size)} from this shared link?`, prompt: $t('remove_assets_shared_link_confirmation', { values: { count: getAssets().size } }),
confirmText: 'Remove', confirmText: $t('remove'),
}); });
if (!isConfirmed) { if (!isConfirmed) {
@ -46,12 +46,12 @@
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Removed ${count} assets`, message: $t('assets_removed_count', { values: { count: count } }),
}); });
clearSelect(); clearSelect();
} catch (error) { } catch (error) {
handleError(error, 'Unable to remove assets from shared link'); handleError(error, $t('errors.unable_to_remove_assets_from_shared_link'));
} }
}; };
</script> </script>

View File

@ -27,7 +27,7 @@
onRestore?.(ids); onRestore?.(ids);
notificationController.show({ notificationController.show({
message: `Restored ${ids.length}`, message: $t('assets_restored_count', { values: { count: ids.length } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });

View File

@ -14,7 +14,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { locale } from '$lib/stores/preferences.store';
import type { AssetResponseDto } from '@immich/sdk'; import type { AssetResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js'; import { mdiClose } from '@mdi/js';
import ControlAppBar from '../shared-components/control-app-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte';
@ -33,8 +32,7 @@
<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> <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"> <p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
{$t('selected')} {$t('selected_count', { values: { count: assets.size } })}
{assets.size.toLocaleString($locale)}
</p> </p>
<slot slot="trailing" /> <slot slot="trailing" />
</ControlAppBar> </ControlAppBar>

View File

@ -3,7 +3,6 @@
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte'; import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store'; import { showDeleteModal } from '$lib/stores/preferences.store';
import Checkbox from '$lib/components/elements/checkbox.svelte'; import Checkbox from '$lib/components/elements/checkbox.svelte';
import { s } from '$lib/utils';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let size: number; export let size: number;
@ -24,7 +23,7 @@
</script> </script>
<ConfirmDialog <ConfirmDialog
title="Permanently delete asset{s(size)}" title={$t('permanently_delete_assets_count', { values: { count: size } })}
confirmText={$t('delete')} confirmText={$t('delete')}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onCancel={() => dispatch('cancel')} onCancel={() => dispatch('cancel')}
@ -38,10 +37,10 @@
this asset? This will also remove it from its album(s). this asset? This will also remove it from its album(s).
{/if} {/if}
</p> </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"> <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> </div>
</svelte:fragment> </svelte:fragment>
</ConfirmDialog> </ConfirmDialog>

View File

@ -57,11 +57,11 @@
const added = data.filter((item) => item.success).length; const added = data.filter((item) => item.success).length;
notificationController.show({ notificationController.show({
message: `Added ${added} assets`, message: $t('assets_added_count', { values: { count: added } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
handleError(error, 'Unable to add assets to shared link'); handleError(error, $t('errors.unable_to_add_assets_to_shared_link'));
} }
}; };

View File

@ -99,17 +99,16 @@
{#if !shared} {#if !shared}
<p class="px-5 py-3 text-xs"> <p class="px-5 py-3 text-xs">
{#if search.length === 0}ALL {(search.length === 0 ? $t('all_albums') : $t('albums')).toUpperCase()}
{/if}ALBUMS
</p> </p>
{/if} {/if}
{#each filteredAlbums as album (album.id)} {#each filteredAlbums as album (album.id)}
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} /> <AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
{/each} {/each}
{:else if albums.length > 0} {: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} {: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} {/if}
</div> </div>
{/if} {/if}

View File

@ -89,7 +89,7 @@
// skip error when a newer search is happening // skip error when a newer search is happening
if (latestSearchTimeout === searchTimeout) { if (latestSearchTimeout === searchTimeout) {
places = []; places = [];
handleError(error, $t('cant_search_places')); handleError(error, $t('errors.cant_search_places'));
showLoadingSpinner = false; showLoadingSpinner = false;
} }
}); });

View File

@ -228,7 +228,7 @@
id={`${listboxId}-${0}`} id={`${listboxId}-${0}`}
on:click={() => closeDropdown()} on:click={() => closeDropdown()}
> >
No results {$t('no_results')}
</li> </li>
{/if} {/if}
{#each filteredOptions as option, index (option.label)} {#each filteredOptions as option, index (option.label)}

View File

@ -102,7 +102,7 @@
sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key); sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
dispatch('created'); dispatch('created');
} catch (error) { } catch (error) {
handleError(error, 'Failed to create shared link'); handleError(error, $t('errors.failed_to_create_shared_link'));
} }
}; };
@ -134,7 +134,7 @@
onClose(); onClose();
} catch (error) { } catch (error) {
handleError(error, 'Failed to edit shared link'); handleError(error, $t('errors.failed_to_edit_shared_link'));
} }
}; };
@ -150,19 +150,18 @@
<section> <section>
{#if shareType === SharedLinkType.Album} {#if shareType === SharedLinkType.Album}
{#if !editingLink} {#if !editingLink}
<div>Let anyone with the link see photos and people in this album.</div> <div>{$t('album_with_link_access')}</div>
{:else} {:else}
<div class="text-sm"> <div class="text-sm">
Public album | <span class="text-immich-primary dark:text-immich-dark-primary" {$t('public_album')} |
>{editingLink.album?.albumName}</span <span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.album?.albumName}</span>
>
</div> </div>
{/if} {/if}
{/if} {/if}
{#if shareType === SharedLinkType.Individual} {#if shareType === SharedLinkType.Individual}
{#if !editingLink} {#if !editingLink}
<div>Let anyone with the link see the selected photo(s)</div> <div>{$t('create_link_to_share_description')}</div>
{:else} {:else}
<div class="text-sm"> <div class="text-sm">
{$t('individual_share')} | {$t('individual_share')} |
@ -204,13 +203,13 @@
<div class="my-3"> <div class="my-3">
<SettingSwitch <SettingSwitch
bind:checked={allowDownload} bind:checked={allowDownload}
title={'Allow public user to download'} title={$t('allow_public_user_to_download')}
disabled={!showMetadata} disabled={!showMetadata}
/> />
</div> </div>
<div class="my-3"> <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>
<div class="text-sm"> <div class="text-sm">

View File

@ -5,7 +5,7 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let title = $t('confirm'); 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 confirmText = $t('confirm');
export let confirmColor: Color = 'red'; export let confirmColor: Color = 'red';
export let cancelText = $t('cancel'); export let cancelText = $t('cancel');

View File

@ -5,6 +5,7 @@
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler } from '$lib/utils/file-uploader'; import { fileUploadHandler } from '$lib/utils/file-uploader';
import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation'; import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation';
import { t } from 'svelte-i18n';
$: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined; $: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined;
$: isShare = isSharedLinkRoute($page.route?.id); $: isShare = isSharedLinkRoute($page.route?.id);
@ -64,6 +65,6 @@
}} }}
> >
<ImmichLogo noText class="m-16 w-48 animate-bounce" /> <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> </div>
{/if} {/if}

View File

@ -13,6 +13,7 @@
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { t } from 'svelte-i18n';
const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>(); const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>();
@ -52,7 +53,7 @@
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id }); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
} }
} catch (error) { } 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 }); await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
} }
} catch (error) { } catch (error) {
handleError(error, 'Cannot navigate to previous asset'); handleError(error, $t('errors.cannot_navigate_previous_asset'));
} }
}; };

View File

@ -187,7 +187,9 @@
src={getAssetThumbnailUrl(feature.properties?.id)} 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" 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 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')} : $t('map_marker_with_image')}
/> />
{/if} {/if}

View File

@ -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'); expect(sut.getByTestId('message')).toHaveTextContent('Notification message');
}); });
}); });

View File

@ -77,7 +77,9 @@
<div class="flex place-items-center gap-2"> <div class="flex place-items-center gap-2">
<Icon path={icon} color={primaryColor[notification.type]} size="20" /> <Icon path={icon} color={primaryColor[notification.type]} size="20" />
<h2 style:color={primaryColor[notification.type]} class="font-medium" data-testid="title"> <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> </h2>
</div> </div>
<CircleIconButton <CircleIconButton

View File

@ -50,7 +50,7 @@
if (await hasTransparentPixels(blob)) { if (await hasTransparentPixels(blob)) {
notificationController.show({ notificationController.show({
type: NotificationType.Error, 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, timeout: 3000,
}); });
return; return;

View File

@ -19,7 +19,7 @@
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1"> <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="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="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> </div>
</fieldset> </fieldset>
</div> </div>

View File

@ -29,7 +29,7 @@
const res = await getAllPeople({ withHidden: false }); const res = await getAllPeople({ withHidden: false });
return orderBySelectedPeopleFirst(res.people); return orderBySelectedPeopleFirst(res.people);
} catch (error) { } catch (error) {
handleError(error, $t('failed_to_get_people')); handleError(error, $t('errors.failed_to_get_people'));
} }
} }
@ -93,10 +93,10 @@
> >
{#if showAllPeople} {#if showAllPeople}
<span><Icon path={mdiClose} ariaHidden /></span> <span><Icon path={mdiClose} ariaHidden /></span>
Collapse {$t('collapse')}
{:else} {:else}
<span><Icon path={mdiArrowRight} ariaHidden /></span> <span><Icon path={mdiArrowRight} ariaHidden /></span>
See all people {$t('see_all_people')}
{/if} {/if}
</Button> </Button>
</div> </div>

View File

@ -21,7 +21,7 @@
on:click={() => dispatch('reset', { default: true })} 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" 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> </button>
{/if} {/if}
</div> </div>

View File

@ -2,6 +2,7 @@
import Checkbox from '$lib/components/elements/checkbox.svelte'; import Checkbox from '$lib/components/elements/checkbox.svelte';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { t } from 'svelte-i18n';
export let value: string[]; export let value: string[];
export let options: { value: string; text: string }[]; export let options: { value: string; text: string }[];
@ -27,7 +28,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -2,6 +2,7 @@
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
import { t } from 'svelte-i18n';
export let title: string; export let title: string;
export let comboboxPlaceholder: string; export let comboboxPlaceholder: string;
@ -23,7 +24,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -2,6 +2,7 @@
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Dropdown, { type RenderedOption } from '$lib/components/elements/dropdown.svelte'; import Dropdown, { type RenderedOption } from '$lib/components/elements/dropdown.svelte';
import { t } from 'svelte-i18n';
export let title: string; export let title: string;
export let subtitle = ''; export let subtitle = '';
@ -23,7 +24,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -12,6 +12,7 @@
import type { FormEventHandler } from 'svelte/elements'; import type { FormEventHandler } from 'svelte/elements';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import PasswordField from '../password-field.svelte'; import PasswordField from '../password-field.svelte';
import { t } from 'svelte-i18n';
export let inputType: SettingInputFieldType; export let inputType: SettingInputFieldType;
export let value: string | number; export let value: string | number;
@ -54,7 +55,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -2,6 +2,7 @@
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
export let value: string | number; export let value: string | number;
export let options: { value: string | number; text: string }[]; export let options: { value: string | number; text: string }[];
@ -34,7 +35,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -4,6 +4,7 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Slider from '$lib/components/elements/slider.svelte'; import Slider from '$lib/components/elements/slider.svelte';
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { t } from 'svelte-i18n';
export let title: string; export let title: string;
export let subtitle = ''; export let subtitle = '';
@ -31,7 +32,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { t } from 'svelte-i18n';
export let value: string; export let value: string;
export let label = ''; export let label = '';
@ -26,7 +27,7 @@
transition:fly={{ x: 10, duration: 200, easing: quintOut }} transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900" class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
> >
Unsaved change {$t('unsaved_change')}
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -19,7 +19,7 @@
const shortcuts: Shortcuts = { const shortcuts: Shortcuts = {
general: [ general: [
{ key: ['←', '→'], action: $t('previous_or_next_photo') }, { 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('search_your_photos') },
{ key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') }, { key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') },
], ],
@ -30,7 +30,7 @@
{ key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') }, { key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') },
{ key: ['⇧', 'd'], action: $t('download') }, { key: ['⇧', 'd'], action: $t('download') },
{ key: ['Space'], action: $t('play_or_pause_video') }, { 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<{ const dispatch = createEventDispatcher<{

View File

@ -8,7 +8,6 @@
import { uploadExecutionQueue } from '$lib/utils/file-uploader'; import { uploadExecutionQueue } from '$lib/utils/file-uploader';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { mdiCog, mdiWindowMinimize, mdiCancel, mdiCloudUploadOutline } from '@mdi/js'; import { mdiCog, mdiWindowMinimize, mdiCancel, mdiCloudUploadOutline } from '@mdi/js';
import { s } from '$lib/utils';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
let showDetail = false; let showDetail = false;
@ -38,18 +37,18 @@
on:outroend={() => { on:outroend={() => {
if ($errorCounter > 0) { if ($errorCounter > 0) {
notificationController.show({ 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, type: NotificationType.Warning,
}); });
} else if ($successCounter > 0) { } else if ($successCounter > 0) {
notificationController.show({ notificationController.show({
message: 'Upload success, refresh the page to see new upload assets.', message: $t('upload_success'),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} }
if ($duplicateCounter > 0) { if ($duplicateCounter > 0) {
notificationController.show({ notificationController.show({
message: `Skipped ${$duplicateCounter} duplicate asset${s($duplicateCounter)}`, message: $t('upload_skipped_duplicates', { values: { count: $duplicateCounter } }),
type: NotificationType.Warning, type: NotificationType.Warning,
}); });
} }
@ -65,12 +64,18 @@
<div class="place-item-center mb-4 flex justify-between"> <div class="place-item-center mb-4 flex justify-between">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="immich-form-label text-xm"> <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>
<p class="immich-form-label text-xs"> <p class="immich-form-label text-xs">
Uploaded <span class="text-immich-success">{$successCounter}</span> - Error {$t('upload_status_uploaded')} <span class="text-immich-success">{$successCounter}</span> -
<span class="text-immich-error">{$errorCounter}</span> {$t('upload_status_errors')} <span class="text-immich-error">{$errorCounter}</span> -
- Duplicates <span class="text-immich-warning">{$duplicateCounter}</span> {$t('upload_status_duplicates')} <span class="text-immich-warning">{$duplicateCounter}</span>
</p> </p>
</div> </div>
<div class="flex flex-col items-end"> <div class="flex flex-col items-end">

View File

@ -35,7 +35,7 @@
</script> </script>
{#if showModal} {#if showModal}
<FullScreenModal title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}> <FullScreenModal title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)}>
<div> <div>
<FormatMessage key="version_announcement_message" let:tag let:message> <FormatMessage key="version_announcement_message" let:tag let:message>
{#if tag === 'link'} {#if tag === 'link'}
@ -53,9 +53,9 @@
<div class="mt-4 font-medium">Your friend, Alex</div> <div class="mt-4 font-medium">Your friend, Alex</div>
<div class="font-sm mt-8"> <div class="font-sm mt-8">
<code>Server Version: {serverVersion}</code> <code>{$t('server_version')}: {serverVersion}</code>
<br /> <br />
<code>Latest Version: {releaseVersion}</code> <code>{$t('latest_version')}: {releaseVersion}</code>
</div> </div>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">

View File

@ -30,13 +30,13 @@
expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject(); expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject();
if (expirationCountdown.days && expirationCountdown.days > 0) { 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) { } 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) { } 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) { } 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> <p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p>
{:else} {:else}
<p> <p>
Expires {getCountDownExpirationDate()} {$t('expires_date', { values: { date: getCountDownExpirationDate() } })}
</p> </p>
{/if} {/if}
{:else} {:else}
<p>Expires ∞</p> <p>{$t('expires_date', { values: { date: '∞' } })}</p>
{/if} {/if}
</div> </div>
@ -97,7 +97,7 @@
<div <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" 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> </div>
{/if} {/if}
@ -105,7 +105,7 @@
<div <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" 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> </div>
{/if} {/if}
@ -113,7 +113,7 @@
<div <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" 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> </div>
{/if} {/if}
@ -121,7 +121,7 @@
<div <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" 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> </div>
{/if} {/if}
</div> </div>

View File

@ -17,7 +17,7 @@
const handleDelete = async (device: SessionResponseDto) => { const handleDelete = async (device: SessionResponseDto) => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'log-out-device', id: 'log-out-device',
prompt: 'Are you sure you want to log out this device?', prompt: $t('logout_this_device_confirmation'),
}); });
if (!isConfirmed) { if (!isConfirmed) {
@ -26,9 +26,9 @@
try { try {
await deleteSession({ id: device.id }); 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) { } catch (error) {
handleError(error, 'Unable to log out device'); handleError(error, $t('errors.unable_to_log_out_device'));
} finally { } finally {
await refresh(); await refresh();
} }
@ -37,7 +37,7 @@
const handleDeleteAll = async () => { const handleDeleteAll = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'log-out-all-devices', id: 'log-out-all-devices',
prompt: 'Are you sure you want to log out all devices?', prompt: $t('logout_all_device_confirmation'),
}); });
if (!isConfirmed) { if (!isConfirmed) {
@ -47,11 +47,11 @@
try { try {
await deleteAllSessions(); await deleteAllSessions();
notificationController.show({ notificationController.show({
message: `Logged out all devices`, message: $t('logged_out_all_devices'),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
handleError(error, 'Unable to log out all devices'); handleError(error, $t('errors.unable_to_log_out_all_devices'));
} finally { } finally {
await refresh(); await refresh();
} }

View File

@ -32,9 +32,9 @@
$preferences.emailNotifications.albumInvite = data.emailNotifications.albumInvite; $preferences.emailNotifications.albumInvite = data.emailNotifications.albumInvite;
$preferences.emailNotifications.albumUpdate = data.emailNotifications.albumUpdate; $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) { } catch (error) {
handleError(error, 'Unable to update settings'); handleError(error, $t('errors.unable_to_update_settings'));
} }
}; };
</script> </script>

View File

@ -63,7 +63,7 @@
{/each} {/each}
{:else} {:else}
<p class="py-5 text-sm"> <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> </p>
{/if} {/if}

View File

@ -6,6 +6,7 @@
"actions": "Actions", "actions": "Actions",
"active": "Active", "active": "Active",
"activity": "Activity", "activity": "Activity",
"activity_changed": "Activity is {enabled, select, true {enabled} other {disabled}}",
"add": "Add", "add": "Add",
"add_a_description": "Add a description", "add_a_description": "Add a description",
"add_a_location": "Add a location", "add_a_location": "Add a location",
@ -21,6 +22,9 @@
"add_to": "Add to...", "add_to": "Add to...",
"add_to_album": "Add to album", "add_to_album": "Add to album",
"add_to_shared_album": "Add to shared 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": { "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/**\".", "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", "authentication_settings": "Authentication Settings",
@ -31,7 +35,7 @@
"cleared_jobs": "Cleared jobs for: {job}", "cleared_jobs": "Cleared jobs for: {job}",
"config_set_by_file": "Config is currently set by a config file", "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": "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_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_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?", "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
@ -66,8 +70,8 @@
"job_settings": "Job Settings", "job_settings": "Job Settings",
"job_settings_description": "Manage job concurrency", "job_settings_description": "Manage job concurrency",
"job_status": "Job Status", "job_status": "Job Status",
"jobs_delayed": "{jobCount} delayed", "jobs_delayed": "{jobCount, plural, other {# delayed}}",
"jobs_failed": "{jobCount} failed", "jobs_failed": "{jobCount, plural, other {# failed}}",
"library_created": "Created library: {library}", "library_created": "Created library: {library}",
"library_cron_expression": "Cron expression", "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>", "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", "paths_validated_successfully": "All paths validated successfully",
"quota_size_gib": "Quota Size (GiB)", "quota_size_gib": "Quota Size (GiB)",
"refreshing_all_libraries": "Refreshing all libraries", "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", "removing_offline_files": "Removing Offline Files",
"repair_all": "Repair All", "repair_all": "Repair All",
"repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}",
@ -203,7 +209,7 @@
"slideshow_duration_description": "Number of seconds to display each image", "slideshow_duration_description": "Number of seconds to display each image",
"smart_search_job_description": "Run machine learning on assets to support smart search", "smart_search_job_description": "Run machine learning on assets to support smart search",
"storage_template_enable_description": "Enable storage template engine", "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_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": "Storage template migration",
"storage_template_migration_description": "Apply the current <link>{template}</link> to previously uploaded assets", "storage_template_migration_description": "Apply the current <link>{template}</link> to previously uploaded assets",
@ -308,21 +314,39 @@
"admin_password": "Admin Password", "admin_password": "Admin Password",
"administration": "Administration", "administration": "Administration",
"advanced": "Advanced", "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": "Album added",
"album_added_notification_setting_description": "Receive an email notification when you are added to a shared album", "album_added_notification_setting_description": "Receive an email notification when you are added to a shared album",
"album_cover_updated": "Album cover updated", "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_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_name": "Album Name",
"album_options": "Album options", "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": "Album updated",
"album_updated_setting_description": "Receive an email notification when a shared album has new assets", "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": "Albums",
"albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}",
"all": "All", "all": "All",
"all_albums": "All albums",
"all_people": "All people", "all_people": "All people",
"all_videos": "All videos",
"allow_dark_mode": "Allow dark mode", "allow_dark_mode": "Allow dark mode",
"allow_edits": "Allow edits", "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": "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", "api_keys": "API Keys",
"app_settings": "App Settings", "app_settings": "App Settings",
"appears_in": "Appears in", "appears_in": "Appears in",
@ -330,34 +354,50 @@
"archive_or_unarchive_photo": "Archive or unarchive photo", "archive_or_unarchive_photo": "Archive or unarchive photo",
"archive_size": "Archive Size", "archive_size": "Archive Size",
"archive_size_description": "Configure the archive size for downloads (in GiB)", "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": "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": "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", "authorized_devices": "Authorized Devices",
"back": "Back", "back": "Back",
"back_close_deselect": "Back, close, or deselect",
"backward": "Backward", "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", "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_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} duplicate assets? This will resolve all duplicate groups without deleting anything.", "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} duplicate assets? This will keep the largest asset of each group and trash all other duplicates.", "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": "Camera",
"camera_brand": "Camera brand", "camera_brand": "Camera brand",
"camera_model": "Camera model", "camera_model": "Camera model",
"cancel": "Cancel", "cancel": "Cancel",
"cancel_search": "Cancel search", "cancel_search": "Cancel search",
"cannot_merge_people": "Cannot merge people", "cannot_merge_people": "Cannot merge people",
"cannot_undo_this_action": "You cannot undo this action!",
"cannot_update_the_description": "Cannot update the description", "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_date": "Change date",
"change_expiration_time": "Change expiration time", "change_expiration_time": "Change expiration time",
"change_location": "Change location", "change_location": "Change location",
"change_name": "Change name", "change_name": "Change name",
"change_name_successfully": "Change name successfully", "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", "change_your_password": "Change your password",
"changed_visibility_successfully": "Changed visibility successfully", "changed_visibility_successfully": "Changed visibility successfully",
"check_all": "Check All", "check_all": "Check All",
@ -369,9 +409,12 @@
"clear_message": "Clear message", "clear_message": "Clear message",
"clear_value": "Clear value", "clear_value": "Clear value",
"close": "Close", "close": "Close",
"collapse": "Collapse",
"collapse_all": "Collapse all", "collapse_all": "Collapse all",
"color_theme": "Color theme", "color_theme": "Color theme",
"comment_deleted": "Comment deleted",
"comment_options": "Comment options", "comment_options": "Comment options",
"comments_and_likes": "Comments & likes",
"comments_are_disabled": "Comments are disabled", "comments_are_disabled": "Comments are disabled",
"confirm": "Confirm", "confirm": "Confirm",
"confirm_admin_password": "Confirm Admin Password", "confirm_admin_password": "Confirm Admin Password",
@ -397,7 +440,9 @@
"create_library": "Create Library", "create_library": "Create Library",
"create_link": "Create link", "create_link": "Create link",
"create_link_to_share": "Create link to share", "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": "Create new person",
"create_new_person_hint": "Assign selected assets to a new person",
"create_new_user": "Create new user", "create_new_user": "Create new user",
"create_user": "Create user", "create_user": "Create user",
"created": "Created", "created": "Created",
@ -435,14 +480,18 @@
"display_order": "Display order", "display_order": "Display order",
"display_original_photos": "Display original photos", "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.", "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", "done": "Done",
"download": "Download", "download": "Download",
"download_settings": "Download", "download_settings": "Download",
"download_settings_description": "Manage settings related to asset download", "download_settings_description": "Manage settings related to asset download",
"downloading": "Downloading", "downloading": "Downloading",
"downloading_asset_filename": "Downloading asset {filename}",
"drop_files_to_upload": "Drop files anywhere to upload",
"duplicates": "Duplicates", "duplicates": "Duplicates",
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates", "duplicates_description": "Resolve each group by indicating which, if any, are duplicates",
"duration": "Duration", "duration": "Duration",
"edit": "Edit",
"edit_album": "Edit album", "edit_album": "Edit album",
"edit_avatar": "Edit avatar", "edit_avatar": "Edit avatar",
"edit_date": "Edit date", "edit_date": "Edit date",
@ -459,52 +508,99 @@
"edit_title": "Edit Title", "edit_title": "Edit Title",
"edit_user": "Edit user", "edit_user": "Edit user",
"edited": "Edited", "edited": "Edited",
"editor": "Editor",
"email": "Email", "email": "Email",
"empty_trash": "Empty trash", "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", "end_date": "End date",
"error": "Error", "error": "Error",
"error_loading_image": "Error loading image", "error_loading_image": "Error loading image",
"error_title": "Error - Something went wrong",
"errors": { "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.", "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.", "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", "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", "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}}", "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_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_comment": "Unable to add comment",
"unable_to_add_exclusion_pattern": "Unable to add exclusion pattern", "unable_to_add_exclusion_pattern": "Unable to add exclusion pattern",
"unable_to_add_import_path": "Unable to add import path", "unable_to_add_import_path": "Unable to add import path",
"unable_to_add_partners": "Unable to add partners", "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_album_user_role": "Unable to change the album user's role",
"unable_to_change_date": "Unable to change date", "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_location": "Unable to change location",
"unable_to_change_password": "Unable to change password", "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_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_api_key": "Unable to create a new API Key",
"unable_to_create_library": "Unable to create library", "unable_to_create_library": "Unable to create library",
"unable_to_create_user": "Unable to create user", "unable_to_create_user": "Unable to create user",
"unable_to_delete_album": "Unable to delete album", "unable_to_delete_album": "Unable to delete album",
"unable_to_delete_asset": "Unable to delete asset", "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_exclusion_pattern": "Unable to delete exclusion pattern",
"unable_to_delete_import_path": "Unable to delete import path", "unable_to_delete_import_path": "Unable to delete import path",
"unable_to_delete_shared_link": "Unable to delete shared link", "unable_to_delete_shared_link": "Unable to delete shared link",
"unable_to_delete_user": "Unable to delete user", "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_exclusion_pattern": "Unable to edit exclusion pattern",
"unable_to_edit_import_path": "Unable to edit import path", "unable_to_edit_import_path": "Unable to edit import path",
"unable_to_empty_trash": "Unable to empty trash", "unable_to_empty_trash": "Unable to empty trash",
"unable_to_enter_fullscreen": "Unable to enter fullscreen", "unable_to_enter_fullscreen": "Unable to enter fullscreen",
"unable_to_exit_fullscreen": "Unable to exit 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_hide_person": "Unable to hide person",
"unable_to_link_oauth_account": "Unable to link OAuth account", "unable_to_link_oauth_account": "Unable to link OAuth account",
"unable_to_load_album": "Unable to load album", "unable_to_load_album": "Unable to load album",
"unable_to_load_asset_activity": "Unable to load asset activity", "unable_to_load_asset_activity": "Unable to load asset activity",
"unable_to_load_items": "Unable to load items", "unable_to_load_items": "Unable to load items",
"unable_to_load_liked_status": "Unable to load liked status", "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_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_refresh_user": "Unable to refresh user",
"unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_album_users": "Unable to remove users from album",
"unable_to_remove_api_key": "Unable to remove API Key", "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_library": "Unable to remove library",
"unable_to_remove_offline_files": "Unable to remove offline files", "unable_to_remove_offline_files": "Unable to remove offline files",
"unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_partner": "Unable to remove partner",
@ -526,23 +622,26 @@
"unable_to_submit_job": "Unable to submit job", "unable_to_submit_job": "Unable to submit job",
"unable_to_trash_asset": "Unable to trash asset", "unable_to_trash_asset": "Unable to trash asset",
"unable_to_unlink_account": "Unable to unlink account", "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_library": "Unable to update library",
"unable_to_update_location": "Unable to update location", "unable_to_update_location": "Unable to update location",
"unable_to_update_settings": "Unable to update settings", "unable_to_update_settings": "Unable to update settings",
"unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_timeline_display_status": "Unable to update timeline display status",
"unable_to_update_user": "Unable to update user" "unable_to_update_user": "Unable to update user"
}, },
"exif": "Exif",
"exit_slideshow": "Exit Slideshow", "exit_slideshow": "Exit Slideshow",
"expand_all": "Expand all", "expand_all": "Expand all",
"expire_after": "Expire after", "expire_after": "Expire after",
"expired": "Expired", "expired": "Expired",
"expires_date": "Expires {date}",
"explore": "Explore", "explore": "Explore",
"export": "Export", "export": "Export",
"export_as_json": "Export as JSON", "export_as_json": "Export as JSON",
"extension": "Extension", "extension": "Extension",
"external": "External", "external": "External",
"external_libraries": "External Libraries", "external_libraries": "External Libraries",
"failed_to_get_people": "Failed to get people",
"favorite": "Favorite", "favorite": "Favorite",
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites", "favorites": "Favorites",
@ -563,7 +662,11 @@
"go_to_search": "Go to search", "go_to_search": "Go to search",
"go_to_share_page": "Go to share page", "go_to_share_page": "Go to share page",
"group_albums_by": "Group albums by...", "group_albums_by": "Group albums by...",
"group_no": "No grouping",
"group_owner": "Group by owner",
"group_year": "Group by year",
"has_quota": "Has quota", "has_quota": "Has quota",
"hi_user": "Hi {name} ({email})",
"hide_gallery": "Hide gallery", "hide_gallery": "Hide gallery",
"hide_password": "Hide password", "hide_password": "Hide password",
"hide_person": "Hide person", "hide_person": "Hide person",
@ -589,6 +692,7 @@
}, },
"invite_people": "Invite People", "invite_people": "Invite People",
"invite_to_album": "Invite to album", "invite_to_album": "Invite to album",
"items_count": "{count, plural, one {# item} other {# items}}",
"jobs": "Jobs", "jobs": "Jobs",
"keep": "Keep", "keep": "Keep",
"keep_all": "Keep All", "keep_all": "Keep All",
@ -596,12 +700,14 @@
"language": "Language", "language": "Language",
"language_setting_description": "Select your preferred language", "language_setting_description": "Select your preferred language",
"last_seen": "Last seen", "last_seen": "Last seen",
"latest_version": "Latest Version",
"leave": "Leave", "leave": "Leave",
"let_others_respond": "Let others respond", "let_others_respond": "Let others respond",
"level": "Level", "level": "Level",
"library": "Library", "library": "Library",
"library_options": "Library options", "library_options": "Library options",
"light": "Light", "light": "Light",
"like_deleted": "Like deleted",
"link_options": "Link options", "link_options": "Link options",
"link_to_oauth": "Link to OAuth", "link_to_oauth": "Link to OAuth",
"linked_oauth_account": "Linked OAuth account", "linked_oauth_account": "Linked OAuth account",
@ -610,7 +716,12 @@
"loading_search_results_failed": "Loading search results failed", "loading_search_results_failed": "Loading search results failed",
"log_out": "Log out", "log_out": "Log out",
"log_out_all_devices": "Log Out All Devices", "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.", "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", "look": "Look",
"loop_videos": "Loop videos", "loop_videos": "Loop videos",
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.", "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_devices": "Manage your logged-in devices",
"manage_your_oauth_connection": "Manage your OAuth connection", "manage_your_oauth_connection": "Manage your OAuth connection",
"map": "Map", "map": "Map",
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
"map_marker_with_image": "Map marker with image", "map_marker_with_image": "Map marker with image",
"map_settings": "Map settings", "map_settings": "Map settings",
"matches": "Matches", "matches": "Matches",
@ -636,6 +748,7 @@
"merge_people_limit": "You can only merge up to 5 faces at a time", "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_prompt": "Do you want to merge these people? This action is irreversible.",
"merge_people_successfully": "Merge people successfully", "merge_people_successfully": "Merge people successfully",
"merged_people_count": "Merged {count, plural, one {# person} other {# people}}",
"minimize": "Minimize", "minimize": "Minimize",
"minute": "Minute", "minute": "Minute",
"missing": "Missing", "missing": "Missing",
@ -651,11 +764,14 @@
"new_password": "New password", "new_password": "New password",
"new_person": "New person", "new_person": "New person",
"new_user_created": "New user created", "new_user_created": "New user created",
"new_version_available": "NEW VERSION AVAILABLE",
"newest_first": "Newest first", "newest_first": "Newest first",
"next": "Next", "next": "Next",
"next_memory": "Next memory", "next_memory": "Next memory",
"no": "No", "no": "No",
"no_albums_message": "Create an album to organize your photos and videos", "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_archived_assets_message": "Archive photos and videos to hide them from your Photos view",
"no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", "no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO",
"no_duplicates_found": "No duplicates were found.", "no_duplicates_found": "No duplicates were found.",
@ -666,6 +782,7 @@
"no_name": "No Name", "no_name": "No Name",
"no_places": "No places", "no_places": "No places",
"no_results": "No results", "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", "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", "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", "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.", "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"ok": "Ok", "ok": "Ok",
"oldest_first": "Oldest first", "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", "online": "Online",
"only_favorites": "Only favorites", "only_favorites": "Only favorites",
"only_refreshes_modified_files": "Only refreshes modified files", "only_refreshes_modified_files": "Only refreshes modified files",
"open_in_openstreetmap": "Open in OpenStreetMap",
"open_the_search_filters": "Open the search filters", "open_the_search_filters": "Open the search filters",
"options": "Options", "options": "Options",
"or": "or",
"organize_your_library": "Organize your library", "organize_your_library": "Organize your library",
"original": "original",
"other": "Other", "other": "Other",
"other_devices": "Other devices", "other_devices": "Other devices",
"other_variables": "Other variables", "other_variables": "Other variables",
@ -702,9 +827,9 @@
"password_required": "Password Required", "password_required": "Password Required",
"password_reset_success": "Password reset success", "password_reset_success": "Password reset success",
"past_durations": { "past_durations": {
"days": "Past {days, plural, one {day} other {{days, number} days}}", "days": "Past {days, plural, one {day} other {# days}}",
"hours": "Past {hours, plural, one {hour} other {{hours, number} hours}}", "hours": "Past {hours, plural, one {hour} other {# hours}}",
"years": "Past {years, plural, one {year} other {{years, number} years}}" "years": "Past {years, plural, one {year} other {# years}}"
}, },
"path": "Path", "path": "Path",
"pattern": "Pattern", "pattern": "Pattern",
@ -713,14 +838,19 @@
"paused": "Paused", "paused": "Paused",
"pending": "Pending", "pending": "Pending",
"people": "People", "people": "People",
"people_edits_count": "Edited {count, plural, one {# person} other {# people}}",
"people_sidebar_description": "Display a link to People in the sidebar", "people_sidebar_description": "Display a link to People in the sidebar",
"permanent_deletion_warning": "Permanent deletion warning", "permanent_deletion_warning": "Permanent deletion warning",
"permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets", "permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets",
"permanently_delete": "Permanently delete", "permanently_delete": "Permanently delete",
"permanently_delete_assets_count": "Permanently delete {count, plural, one {asset} other {assets}}",
"permanently_deleted_asset": "Permanently deleted asset", "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": "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": "Photos",
"photos_and_videos": "Photos & Videos",
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
"photos_from_previous_years": "Photos from previous years", "photos_from_previous_years": "Photos from previous years",
"pick_a_location": "Pick a location", "pick_a_location": "Pick a location",
@ -738,20 +868,39 @@
"previous_or_next_photo": "Previous or next photo", "previous_or_next_photo": "Previous or next photo",
"primary": "Primary", "primary": "Primary",
"profile_picture_set": "Profile picture set.", "profile_picture_set": "Profile picture set.",
"public_album": "Public album",
"public_share": "Public Share", "public_share": "Public Share",
"reaction_options": "Reaction options", "reaction_options": "Reaction options",
"read_changelog": "Read Changelog", "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": "Recent",
"recent_searches": "Recent searches", "recent_searches": "Recent searches",
"refresh": "Refresh", "refresh": "Refresh",
"refresh_encoded_videos": "Refresh encoded videos",
"refresh_metadata": "Refresh metadata",
"refresh_thumbnails": "Refresh thumbnails",
"refreshed": "Refreshed", "refreshed": "Refreshed",
"refreshes_every_file": "Refreshes every file", "refreshes_every_file": "Refreshes every file",
"refreshing_encoded_video": "Refreshing encoded video",
"refreshing_metadata": "Refreshing metadata",
"regenerating_thumbnails": "Regenerating thumbnails",
"remove": "Remove", "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_album": "Remove from album",
"remove_from_favorites": "Remove from favorites", "remove_from_favorites": "Remove from favorites",
"remove_from_shared_link": "Remove from shared link", "remove_from_shared_link": "Remove from shared link",
"remove_offline_files": "Remove Offline Files", "remove_offline_files": "Remove Offline Files",
"remove_user": "Remove user",
"removed_api_key": "Removed API Key: {name}", "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", "rename": "Rename",
"repair": "Repair", "repair": "Repair",
"repair_no_results_message": "Untracked and missing files will show up here", "repair_no_results_message": "Untracked and missing files will show up here",
@ -761,14 +910,18 @@
"reset": "Reset", "reset": "Reset",
"reset_password": "Reset password", "reset_password": "Reset password",
"reset_people_visibility": "Reset people visibility", "reset_people_visibility": "Reset people visibility",
"reset_to_default": "Reset to default",
"resolved_all_duplicates": "Resolved all duplicates", "resolved_all_duplicates": "Resolved all duplicates",
"restore": "Restore", "restore": "Restore",
"restore_all": "Restore all", "restore_all": "Restore all",
"restore_user": "Restore user", "restore_user": "Restore user",
"restored_asset": "Restored asset",
"resume": "Resume", "resume": "Resume",
"retry_upload": "Retry upload", "retry_upload": "Retry upload",
"review_duplicates": "Review duplicates", "review_duplicates": "Review duplicates",
"role": "Role", "role": "Role",
"role_editor": "Editor",
"role_viewer": "Viewer",
"save": "Save", "save": "Save",
"saved_api_key": "Saved API Key", "saved_api_key": "Saved API Key",
"saved_profile": "Saved profile", "saved_profile": "Saved profile",
@ -787,6 +940,8 @@
"search_city": "Search city...", "search_city": "Search city...",
"search_country": "Search country...", "search_country": "Search country...",
"search_for_existing_person": "Search for existing person", "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_people": "Search people",
"search_places": "Search places", "search_places": "Search places",
"search_state": "Search state...", "search_state": "Search state...",
@ -795,21 +950,25 @@
"search_your_photos": "Search your photos", "search_your_photos": "Search your photos",
"searching_locales": "Searching locales...", "searching_locales": "Searching locales...",
"second": "Second", "second": "Second",
"see_all_people": "See all people",
"select_album_cover": "Select album cover", "select_album_cover": "Select album cover",
"select_all": "Select all", "select_all": "Select all",
"select_avatar_color": "Select avatar color", "select_avatar_color": "Select avatar color",
"select_face": "Select face", "select_face": "Select face",
"select_featured_photo": "Select featured photo", "select_featured_photo": "Select featured photo",
"select_from_computer": "Select from computer",
"select_keep_all": "Select keep all", "select_keep_all": "Select keep all",
"select_library_owner": "Select library owner", "select_library_owner": "Select library owner",
"select_new_face": "Select new face", "select_new_face": "Select new face",
"select_photos": "Select photos", "select_photos": "Select photos",
"select_trash_all": "Select trash all", "select_trash_all": "Select trash all",
"selected": "Selected", "selected": "Selected",
"selected_count": "{count, plural, other {# selected}}",
"send_message": "Send message", "send_message": "Send message",
"send_welcome_email": "Send welcome email", "send_welcome_email": "Send welcome email",
"server": "Server", "server": "Server",
"server_stats": "Server Stats", "server_stats": "Server Stats",
"server_version": "Server Version",
"set": "Set", "set": "Set",
"set_as_album_cover": "Set as album cover", "set_as_album_cover": "Set as album cover",
"set_as_profile_picture": "Set as profile picture", "set_as_profile_picture": "Set as profile picture",
@ -821,13 +980,15 @@
"share": "Share", "share": "Share",
"shared": "Shared", "shared": "Shared",
"shared_by": "Shared by", "shared_by": "Shared by",
"shared_by_user": "Shared by {user}",
"shared_by_you": "Shared by you", "shared_by_you": "Shared by you",
"shared_from_partner": "Photos from {partner}", "shared_from_partner": "Photos from {partner}",
"shared_links": "Shared links", "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}", "shared_with_partner": "Shared with {partner}",
"sharing": "Sharing", "sharing": "Sharing",
"sharing_sidebar_description": "Display a link to Sharing in the sidebar", "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_album_options": "Show album options",
"show_and_hide_people": "Show & hide people", "show_and_hide_people": "Show & hide people",
"show_file_location": "Show file location", "show_file_location": "Show file location",
@ -850,8 +1011,15 @@
"slideshow": "Slideshow", "slideshow": "Slideshow",
"slideshow_settings": "Slideshow settings", "slideshow_settings": "Slideshow settings",
"sort_albums_by": "Sort albums by...", "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": "Stack",
"stack_selected_photos": "Stack selected photos", "stack_selected_photos": "Stack selected photos",
"stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}",
"stacktrace": "Stacktrace", "stacktrace": "Stacktrace",
"start": "Start", "start": "Start",
"start_date": "Start date", "start_date": "Start date",
@ -873,10 +1041,13 @@
"theme": "Theme", "theme": "Theme",
"theme_selection": "Theme selection", "theme_selection": "Theme selection",
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference", "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", "time_based_memories": "Time-based memories",
"timezone": "Timezone", "timezone": "Timezone",
"to_archive": "Archive", "to_archive": "Archive",
"to_change_password": "Change password",
"to_favorite": "Favorite", "to_favorite": "Favorite",
"to_login": "Login",
"to_trash": "Trash", "to_trash": "Trash",
"toggle_settings": "Toggle settings", "toggle_settings": "Toggle settings",
"toggle_theme": "Toggle theme", "toggle_theme": "Toggle theme",
@ -885,11 +1056,12 @@
"trash": "Trash", "trash": "Trash",
"trash_all": "Trash All", "trash_all": "Trash All",
"trash_count": "Trash {count}", "trash_count": "Trash {count}",
"trash_delete_asset": "Trash/Delete Asset",
"trash_no_results_message": "Trashed photos and videos will show up here.", "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}}.", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
"type": "Type", "type": "Type",
"unarchive": "Unarchive", "unarchive": "Unarchive",
"unarchived": "Unarchived", "unarchived_count": "{count, plural, other {Unarchived #}}",
"unfavorite": "Unfavorite", "unfavorite": "Unfavorite",
"unhide_person": "Unhide person", "unhide_person": "Unhide person",
"unknown": "Unknown", "unknown": "Unknown",
@ -899,18 +1071,30 @@
"unlinked_oauth_account": "Unlinked OAuth account", "unlinked_oauth_account": "Unlinked OAuth account",
"unnamed_album": "Unnamed Album", "unnamed_album": "Unnamed Album",
"unnamed_share": "Unnamed Share", "unnamed_share": "Unnamed Share",
"unsaved_change": "Unsaved change",
"unselect_all": "Unselect all", "unselect_all": "Unselect all",
"unstack": "Un-stack", "unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untracked_files": "Untracked files", "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", "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", "up_next": "Up next",
"updated_password": "Updated password", "updated_password": "Updated password",
"upload": "Upload", "upload": "Upload",
"upload_concurrency": "Upload concurrency", "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", "url": "URL",
"usage": "Usage", "usage": "Usage",
"use_custom_date_range": "Use custom date range instead",
"user": "User", "user": "User",
"user_id": "User ID", "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", "user_usage_detail": "User usage detail",
"username": "Username", "username": "Username",
"users": "Users", "users": "Users",
@ -925,17 +1109,21 @@
"videos": "Videos", "videos": "Videos",
"videos_count": "{count, plural, one {# Video} other {# Videos}}", "videos_count": "{count, plural, one {# Video} other {# Videos}}",
"view": "View", "view": "View",
"view_album": "View Album",
"view_all": "View All", "view_all": "View All",
"view_all_users": "View all users", "view_all_users": "View all users",
"view_links": "View links", "view_links": "View links",
"view_next_asset": "View next asset", "view_next_asset": "View next asset",
"view_previous_asset": "View previous 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", "waiting": "Waiting",
"warning": "Warning",
"week": "Week", "week": "Week",
"welcome": "Welcome", "welcome": "Welcome",
"welcome_to_immich": "Welcome to immich", "welcome_to_immich": "Welcome to immich",
"year": "Year", "year": "Year",
"years_ago": "{years, plural, one {# year} other {# years}} ago",
"yes": "Yes", "yes": "Yes",
"you_dont_have_any_shared_links": "You don't have any shared links", "you_dont_have_any_shared_links": "You don't have any shared links",
"zoom_image": "Zoom Image" "zoom_image": "Zoom Image"

View File

@ -197,25 +197,29 @@ export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileIm
export const getPeopleThumbnailUrl = (personId: string) => createUrl(getPeopleThumbnailPath(personId)); export const getPeopleThumbnailUrl = (personId: string) => createUrl(getPeopleThumbnailPath(personId));
export const getAssetJobName = (job: AssetJobName) => { export const getAssetJobName = derived(t, ($t) => {
return (job: AssetJobName) => {
const names: Record<AssetJobName, string> = { const names: Record<AssetJobName, string> = {
[AssetJobName.RefreshMetadata]: 'Refresh metadata', [AssetJobName.RefreshMetadata]: $t('refresh_metadata'),
[AssetJobName.RegenerateThumbnail]: 'Refresh thumbnails', [AssetJobName.RegenerateThumbnail]: $t('refresh_thumbnails'),
[AssetJobName.TranscodeVideo]: 'Refresh encoded videos', [AssetJobName.TranscodeVideo]: $t('refresh_encoded_videos'),
}; };
return names[job]; return names[job];
}; };
});
export const getAssetJobMessage = (job: AssetJobName) => { export const getAssetJobMessage = derived(t, ($t) => {
return (job: AssetJobName) => {
const messages: Record<AssetJobName, string> = { const messages: Record<AssetJobName, string> = {
[AssetJobName.RefreshMetadata]: 'Refreshing metadata', [AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
[AssetJobName.RegenerateThumbnail]: `Regenerating thumbnails`, [AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
[AssetJobName.TranscodeVideo]: `Refreshing encoded video`, [AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
}; };
return messages[job]; return messages[job];
}; };
});
export const getAssetJobIcon = (job: AssetJobName) => { export const getAssetJobIcon = (job: AssetJobName) => {
const names: Record<AssetJobName, string> = { const names: Record<AssetJobName, string> = {
@ -261,13 +265,14 @@ export const oauth = {
return false; return false;
}, },
authorize: async (location: Location) => { authorize: async (location: Location) => {
const $t = get(t);
try { try {
const redirectUri = location.href.split('?')[0]; const redirectUri = location.href.split('?')[0];
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } }); const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } });
window.location.href = url; window.location.href = url;
return true; return true;
} catch (error) { } catch (error) {
handleError(error, 'Unable to login with OAuth'); handleError(error, $t('errors.unable_to_login_with_oauth'));
return false; return false;
} }
}, },
@ -302,7 +307,10 @@ export const handlePromiseError = <T>(promise: Promise<T>): void => {
export const s = (count: number) => (count === 1 ? '' : 's'); 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]> => { export const withError = async <T>(fn: () => Promise<T>): Promise<[undefined, T] | [unknown, undefined]> => {
try { try {

View File

@ -1,5 +1,7 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk'; import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
export type OnDelete = (assetIds: string[]) => void; export type OnDelete = (assetIds: string[]) => void;
@ -10,15 +12,18 @@ export type OnStack = (ids: string[]) => void;
export type OnUnstack = (assets: AssetResponseDto[]) => void; export type OnUnstack = (assets: AssetResponseDto[]) => void;
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
const $t = get(t);
try { try {
await deleteBulk({ assetBulkDeleteDto: { ids, force } }); await deleteBulk({ assetBulkDeleteDto: { ids, force } });
onAssetDelete(ids); onAssetDelete(ids);
notificationController.show({ 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, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
handleError(error, 'Error deleting assets'); handleError(error, $t('errors.unable_to_delete_assets'));
} }
}; };

View File

@ -1,6 +1,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { import {
AlbumFilter,
AlbumGroupBy, AlbumGroupBy,
AlbumSortBy, AlbumSortBy,
SortOrder, SortOrder,
@ -10,6 +11,7 @@ import {
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import type { AlbumResponseDto } from '@immich/sdk'; import type { AlbumResponseDto } from '@immich/sdk';
import * as sdk from '@immich/sdk'; import * as sdk from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
/** /**
@ -27,7 +29,8 @@ export const createAlbum = async (name?: string, assetIds?: string[]) => {
}); });
return newAlbum; return newAlbum;
} catch (error) { } 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 { export interface AlbumSortOptionMetadata {
id: AlbumSortBy; id: AlbumSortBy;
text: string;
defaultOrder: SortOrder; defaultOrder: SortOrder;
columnStyle: string; columnStyle: string;
} }
@ -53,37 +55,31 @@ export interface AlbumSortOptionMetadata {
export const sortOptionsMetadata: AlbumSortOptionMetadata[] = [ export const sortOptionsMetadata: AlbumSortOptionMetadata[] = [
{ {
id: AlbumSortBy.Title, id: AlbumSortBy.Title,
text: 'Title',
defaultOrder: SortOrder.Asc, 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%]', 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, id: AlbumSortBy.ItemCount,
text: 'Number of items',
defaultOrder: SortOrder.Desc, defaultOrder: SortOrder.Desc,
columnStyle: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]', columnStyle: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]',
}, },
{ {
id: AlbumSortBy.DateModified, id: AlbumSortBy.DateModified,
text: 'Date modified',
defaultOrder: SortOrder.Desc, defaultOrder: SortOrder.Desc,
columnStyle: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', columnStyle: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
}, },
{ {
id: AlbumSortBy.DateCreated, id: AlbumSortBy.DateCreated,
text: 'Date created',
defaultOrder: SortOrder.Desc, defaultOrder: SortOrder.Desc,
columnStyle: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', columnStyle: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
}, },
{ {
id: AlbumSortBy.MostRecentPhoto, id: AlbumSortBy.MostRecentPhoto,
text: 'Most recent photo',
defaultOrder: SortOrder.Desc, defaultOrder: SortOrder.Desc,
columnStyle: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', columnStyle: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
}, },
{ {
id: AlbumSortBy.OldestPhoto, id: AlbumSortBy.OldestPhoto,
text: 'Oldest photo',
defaultOrder: SortOrder.Desc, defaultOrder: SortOrder.Desc,
columnStyle: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', 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; 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 * Album Grouping
@ -108,7 +110,6 @@ export interface AlbumGroup {
export interface AlbumGroupOptionMetadata { export interface AlbumGroupOptionMetadata {
id: AlbumGroupBy; id: AlbumGroupBy;
text: string;
defaultOrder: SortOrder; defaultOrder: SortOrder;
isDisabled: () => boolean; isDisabled: () => boolean;
} }
@ -116,13 +117,11 @@ export interface AlbumGroupOptionMetadata {
export const groupOptionsMetadata: AlbumGroupOptionMetadata[] = [ export const groupOptionsMetadata: AlbumGroupOptionMetadata[] = [
{ {
id: AlbumGroupBy.None, id: AlbumGroupBy.None,
text: 'No grouping',
defaultOrder: SortOrder.Asc, defaultOrder: SortOrder.Asc,
isDisabled: () => false, isDisabled: () => false,
}, },
{ {
id: AlbumGroupBy.Year, id: AlbumGroupBy.Year,
text: 'Group by year',
defaultOrder: SortOrder.Desc, defaultOrder: SortOrder.Desc,
isDisabled() { isDisabled() {
const disabledWithSortOptions: string[] = [AlbumSortBy.DateCreated, AlbumSortBy.DateModified]; const disabledWithSortOptions: string[] = [AlbumSortBy.DateCreated, AlbumSortBy.DateModified];
@ -131,7 +130,6 @@ export const groupOptionsMetadata: AlbumGroupOptionMetadata[] = [
}, },
{ {
id: AlbumGroupBy.Owner, id: AlbumGroupBy.Owner,
text: 'Group by owner',
defaultOrder: SortOrder.Asc, defaultOrder: SortOrder.Asc,
isDisabled: () => false, isDisabled: () => false,
}, },

View File

@ -6,7 +6,7 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
import { downloadManager } from '$lib/stores/download'; import { downloadManager } from '$lib/stores/download';
import { preferences } from '$lib/stores/user.store'; 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 { createAlbum } from '$lib/utils/album-utils';
import { getByteUnitString } from '$lib/utils/byte-units'; import { getByteUnitString } from '$lib/utils/byte-units';
import { encodeHTMLSpecialChars } from '$lib/utils/string-utils'; import { encodeHTMLSpecialChars } from '$lib/utils/string-utils';
@ -24,7 +24,7 @@ import {
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { t as translate } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
@ -37,15 +37,16 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[]) => {
key: getKey(), key: getKey(),
}); });
const count = result.filter(({ success }) => success).length; const count = result.filter(({ success }) => success).length;
const $t = get(t);
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
timeout: 5000, timeout: 5000,
message: message:
count > 0 count > 0
? `Added ${count} asset${s(count)} to the album` ? $t('assets_added_to_album_count', { values: { count: count } })
: `Asset${assetIds.length === 1 ? ' was' : 's were'} already part of the album`, : $t('assets_were_part_of_album_count', { values: { count: assetIds.length } }),
button: { button: {
text: 'View Album', text: $t('view_album'),
onClick() { onClick() {
return goto(`${AppRoute.ALBUMS}/${albumId}`); return goto(`${AppRoute.ALBUMS}/${albumId}`);
}, },
@ -59,13 +60,14 @@ export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[])
return; return;
} }
const displayName = albumName ? `<b>${encodeHTMLSpecialChars(albumName)}</b>` : 'new album'; const displayName = albumName ? `<b>${encodeHTMLSpecialChars(albumName)}</b>` : 'new album';
const $t = get(t);
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
timeout: 5000, 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, html: true,
button: { button: {
text: 'View Album', text: $t('view_album'),
onClick() { onClick() {
return goto(`${AppRoute.ALBUMS}/${album.id}`); 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() })); const [error, downloadInfo] = await withError(() => getDownloadInfo({ downloadInfoDto: dto, key: getKey() }));
if (error) { if (error) {
handleError(error, 'Unable to download files'); const $t = get(t);
handleError(error, $t('errors.unable_to_download_files'));
return; return;
} }
@ -134,7 +137,8 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
downloadBlob(data, archiveName); downloadBlob(data, archiveName);
} catch (error) { } catch (error) {
handleError(error, 'Unable to download files'); const $t = get(t);
handleError(error, $t('errors.unable_to_download_files'));
downloadManager.clear(downloadKey); downloadManager.clear(downloadKey);
return; return;
} finally { } finally {
@ -144,10 +148,11 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
}; };
export const downloadFile = async (asset: AssetResponseDto) => { export const downloadFile = async (asset: AssetResponseDto) => {
const $t = get(t);
if (asset.isOffline) { if (asset.isOffline) {
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Asset ${asset.originalFileName} is offline`, message: $t('asset_filename_is_offline', { values: { filename: asset.originalFileName } }),
}); });
return; return;
} }
@ -178,7 +183,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
notificationController.show({ notificationController.show({
type: NotificationType.Info, 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 // TODO use sdk once it supports progress events
@ -191,7 +196,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
downloadBlob(data, filename); downloadBlob(data, filename);
} catch (error) { } catch (error) {
handleError(error, `Error downloading ${filename}`); handleError(error, $t('errors.error_downloading', { values: { filename: filename } }));
downloadManager.clear(downloadKey); downloadManager.clear(downloadKey);
} finally { } finally {
setTimeout(() => downloadManager.clear(downloadKey), 5000); 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; const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
if (numberOfIssues > 0) { if (numberOfIssues > 0) {
const $t = get(t);
notificationController.show({ 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, type: NotificationType.Warning,
}); });
} }
@ -318,6 +324,7 @@ export const stackAssets = async (assets: AssetResponseDto[]) => {
const parent = assets[0]; const parent = assets[0];
const children = assets.slice(1); const children = assets.slice(1);
const ids = children.map(({ id }) => id); const ids = children.map(({ id }) => id);
const $t = get(t);
try { try {
await updateAssets({ await updateAssets({
@ -327,7 +334,7 @@ export const stackAssets = async (assets: AssetResponseDto[]) => {
}, },
}); });
} catch (error) { } catch (error) {
handleError(error, 'Failed to stack assets'); handleError(error, $t('errors.failed_to_stack_assets'));
return false; return false;
} }
@ -348,10 +355,10 @@ export const stackAssets = async (assets: AssetResponseDto[]) => {
parent.stackCount = parent.stack.length + 1; parent.stackCount = parent.stack.length + 1;
notificationController.show({ notificationController.show({
message: `Stacked ${parent.stackCount} assets`, message: $t('stacked_assets_count', { values: { count: parent.stackCount } }),
type: NotificationType.Info, type: NotificationType.Info,
button: { button: {
text: 'View Stack', text: $t('view_stack'),
onClick() { onClick() {
return assetViewingStore.setAssetId(parent.id); return assetViewingStore.setAssetId(parent.id);
}, },
@ -363,6 +370,7 @@ export const stackAssets = async (assets: AssetResponseDto[]) => {
export const unstackAssets = async (assets: AssetResponseDto[]) => { export const unstackAssets = async (assets: AssetResponseDto[]) => {
const ids = assets.map(({ id }) => id); const ids = assets.map(({ id }) => id);
const $t = get(t);
try { try {
await updateAssets({ await updateAssets({
assetBulkUpdateDto: { assetBulkUpdateDto: {
@ -371,7 +379,7 @@ export const unstackAssets = async (assets: AssetResponseDto[]) => {
}, },
}); });
} catch (error) { } catch (error) {
handleError(error, 'Failed to un-stack assets'); handleError(error, $t('errors.failed_to_unstack_assets'));
return; return;
} }
for (const asset of assets) { for (const asset of assets) {
@ -381,7 +389,7 @@ export const unstackAssets = async (assets: AssetResponseDto[]) => {
} }
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Un-stacked ${assets.length} assets`, message: $t('unstacked_assets_count', { values: { count: assets.length } }),
}); });
return assets; return assets;
}; };
@ -409,12 +417,14 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt
await delay(0); await delay(0);
} }
} catch (error) { } catch (error) {
handleError(error, 'Error selecting all assets'); const $t = get(t);
handleError(error, $t('errors.error_selecting_all_assets'));
isSelectingAllAssets.set(false); isSelectingAllAssets.set(false);
} }
}; };
export const toggleArchive = async (asset: AssetResponseDto) => { export const toggleArchive = async (asset: AssetResponseDto) => {
const $t = get(t);
try { try {
const data = await updateAsset({ const data = await updateAsset({
id: asset.id, id: asset.id,
@ -427,10 +437,10 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
notificationController.show({ notificationController.show({
type: NotificationType.Info, 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) { } 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; return asset;
@ -439,6 +449,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => { export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => {
const isArchived = archive; const isArchived = archive;
const ids = assets.map(({ id }) => id); const ids = assets.map(({ id }) => id);
const $t = get(t);
try { try {
if (ids.length > 0) { if (ids.length > 0) {
@ -449,13 +460,14 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
asset.isArchived = isArchived; asset.isArchived = isArchived;
} }
const t = get(translate);
notificationController.show({ 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, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`); handleError(error, $t('errors.unable_to_archive_unarchive', { values: { archived: isArchived } }));
} }
return ids; return ids;

View File

@ -1,4 +1,6 @@
import type { PersonResponseDto } from '@immich/sdk'; import type { PersonResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
export const searchNameLocal = ( export const searchNameLocal = (
name: string, name: string,
@ -26,5 +28,6 @@ export const searchNameLocal = (
}; };
export const getPersonNameWithHiddenValue = (name: string, isHidden: boolean) => { 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 } });
}; };

View File

@ -37,10 +37,9 @@
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.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 { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.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 { downloadAlbum } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@ -168,10 +167,10 @@
}); });
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`, message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }),
}); });
} catch (error) { } 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]; reactions = [...reactions, isLiked];
} }
} catch (error) { } 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 }); const { comments } = await getActivityStatistics({ albumId: album.id });
setNumberOfComments(comments); setNumberOfComments(comments);
} catch (error) { } 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; const count = results.filter(({ success }) => success).length;
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Added ${count} asset${s(count)}`, message: $t('assets_added_count', { values: { count: count } }),
}); });
await refreshAlbum(); await refreshAlbum();
@ -291,7 +290,7 @@
timelineInteractionStore.clearMultiselect(); timelineInteractionStore.clearMultiselect();
viewMode = ViewMode.VIEW; viewMode = ViewMode.VIEW;
} catch (error) { } 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; viewMode = ViewMode.VIEW;
} catch (error) { } catch (error) {
handleError(error, 'Error adding users to album'); handleError(error, $t('errors.error_adding_users_to_album'));
} }
}; };
@ -331,7 +330,7 @@
await refreshAlbum(); await refreshAlbum();
viewMode = album.albumUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW; viewMode = album.albumUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW;
} catch (error) { } 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 handleRemoveAlbum = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'remove-album', 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) { if (!isConfirmed) {
@ -393,7 +392,7 @@
}, },
}); });
} catch (error) { } 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"> <svelte:fragment slot="leading">
<p class="text-lg dark:text-immich-dark-fg"> <p class="text-lg dark:text-immich-dark-fg">
{#if $timelineSelected.size === 0} {#if $timelineSelected.size === 0}
Add to album {$t('add_to_album')}
{:else} {:else}
{$timelineSelected.size.toLocaleString($locale)} selected {$t('selected_count', { values: { count: $timelineSelected.size } })}
{/if} {/if}
</p> </p>
</svelte:fragment> </svelte:fragment>
@ -508,7 +507,7 @@
on:click={handleSelectFromComputer} 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" 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>
<Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} on:click={handleAddAssets} <Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} on:click={handleAddAssets}
>{$t('done')}</Button >{$t('done')}</Button

View File

@ -161,21 +161,16 @@
if (results.length - count > 0) { if (results.length - count > 0) {
notificationController.show({ notificationController.show({
type: NotificationType.Error, type: NotificationType.Error,
message: `Unable to change the visibility for ${results.length - count} ${ message: $t('errors.unable_to_change_visibility', { values: { count: results.length - count } }),
results.length - count <= 1 ? 'person' : 'people'
}`,
}); });
} }
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: `Visibility changed for ${count} ${count <= 1 ? 'person' : 'people'}`, message: $t('visibility_changed', { values: { count: count } }),
}); });
} }
} catch (error) { } catch (error) {
handleError( handleError(error, $t('errors.unable_to_change_visibility', { values: { count: changed.length } }));
error,
`Unable to change the visibility for ${changed.length} ${changed.length <= 1 ? 'person' : 'people'}`,
);
} }
// Reset variables used on the "Show & hide people" modal // Reset variables used on the "Show & hide people" modal
showLoadingSpinner = false; showLoadingSpinner = false;
@ -346,7 +341,7 @@
return person; return person;
}); });
notificationController.show({ notificationController.show({
message: 'Date of birth saved successfully', message: $t('birthdate_saved'),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
@ -447,7 +442,7 @@
<div class="flex flex-col content-center items-center text-center"> <div class="flex flex-col content-center items-center text-center">
<Icon path={mdiAccountOff} size="3.5em" /> <Icon path={mdiAccountOff} size="3.5em" />
<p class="mt-5 text-3xl font-medium max-w-lg line-clamp-2 overflow-hidden"> <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> </p>
</div> </div>
</div> </div>

View File

@ -30,7 +30,7 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store'; import { AssetStore } from '$lib/stores/assets.store';
import { websocketEvents } from '$lib/stores/websocket'; 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 { clickOutside } from '$lib/actions/click-outside';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { isExternalUrl } from '$lib/utils/navigation'; import { isExternalUrl } from '$lib/utils/navigation';
@ -488,7 +488,7 @@
{#if data.person.name} {#if data.person.name}
<p class="w-40 sm:w-72 font-medium truncate">{data.person.name}</p> <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"> <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> </p>
{:else} {:else}
<p class="font-medium">{$t('add_a_name')}</p> <p class="font-medium">{$t('add_a_name')}</p>

View File

@ -279,7 +279,9 @@
<div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">{$t('albums').toUpperCase()}</div> <div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">{$t('albums').toUpperCase()}</div>
<AlbumCardGroup albums={searchResultAlbums} showDateRange showItemCount /> <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> </section>
{/if} {/if}
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg"> <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"> <div class="flex flex-col content-center items-center text-center">
<Icon path={mdiImageOffOutline} size="3.5em" /> <Icon path={mdiImageOffOutline} size="3.5em" />
<p class="mt-5 text-3xl font-medium">{$t('no_results')}</p> <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>
</div> </div>
{/if} {/if}

View File

@ -37,8 +37,7 @@
const handleEmptyTrash = async () => { const handleEmptyTrash = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'empty-trash', id: 'empty-trash',
prompt: prompt: $t('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!',
}); });
if (!isConfirmed) { if (!isConfirmed) {
@ -53,7 +52,7 @@
assetStore.removeAssets(deletedAssetIds); assetStore.removeAssets(deletedAssetIds);
notificationController.show({ notificationController.show({
message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {
@ -64,7 +63,7 @@
const handleRestoreTrash = async () => { const handleRestoreTrash = async () => {
const isConfirmed = await dialogController.show({ const isConfirmed = await dialogController.show({
id: 'restore-trash', 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) { if (!isConfirmed) {
@ -78,7 +77,7 @@
assetStore.removeAssets(restoredAssetIds); assetStore.removeAssets(restoredAssetIds);
notificationController.show({ notificationController.show({
message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`, message: $t('assets_restored_count', { values: { count: numberOfAssets } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
} catch (error) { } catch (error) {

View File

@ -42,8 +42,8 @@
notificationController.show({ notificationController.show({
message: $featureFlags.trash message: $featureFlags.trash
? $t('assets_moved_to_trash', { values: { count: trashedCount } }) ? $t('assets_moved_to_trash_count', { values: { count: trashedCount } })
: $t('permanently_deleted_assets', { values: { count: trashedCount } }), : $t('permanently_deleted_assets_count', { values: { count: trashedCount } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
}; };

View File

@ -35,7 +35,7 @@
<div> <div>
<div class="flex items-center justify-between gap-4 px-4 py-4"> <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"> <h1 class="font-medium text-immich-primary dark:text-immich-dark-primary">
🚨 Error - Something went wrong 🚨 {$t('error_title')}
</h1> </h1>
<div class="flex justify-end"> <div class="flex justify-end">
<CircleIconButton <CircleIconButton

View File

@ -6,6 +6,7 @@
import { resetSavedUser, user } from '$lib/stores/user.store'; import { resetSavedUser, user } from '$lib/stores/user.store';
import { logout } from '@immich/sdk'; import { logout } from '@immich/sdk';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { t } from 'svelte-i18n';
export let data: PageData; export let data: PageData;
@ -18,11 +19,10 @@
<FullscreenContainer title={data.meta.title}> <FullscreenContainer title={data.meta.title}>
<p slot="message"> <p slot="message">
Hi {$user.name} ({$user.email}), {$t('hi_user', { values: { name: $user.name, email: $user.email } })}
<br /> <br />
<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 {$t('change_password_description')}
enter the new password below.
</p> </p>
<ChangePasswordForm on:success={onSuccess} /> <ChangePasswordForm on:success={onSuccess} />

View File

@ -2,6 +2,7 @@ import { AppRoute } from '$lib/constants';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
@ -11,9 +12,11 @@ export const load = (async () => {
redirect(302, AppRoute.PHOTOS); redirect(302, AppRoute.PHOTOS);
} }
const $t = get(t);
return { return {
meta: { meta: {
title: 'Change Password', title: $t('change_password'),
}, },
}; };
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@ -1,6 +1,8 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { defaults, getServerConfig } from '@immich/sdk'; import { defaults, getServerConfig } from '@immich/sdk';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async ({ fetch }) => { export const load = (async ({ fetch }) => {
@ -11,9 +13,10 @@ export const load = (async ({ fetch }) => {
redirect(302, AppRoute.AUTH_REGISTER); redirect(302, AppRoute.AUTH_REGISTER);
} }
const $t = get(t);
return { return {
meta: { meta: {
title: 'Login', title: $t('login'),
}, },
}; };
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@ -1,13 +1,18 @@
import { loadConfig } from '$lib/stores/server-config.store'; import { loadConfig } from '$lib/stores/server-config.store';
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async () => { export const load = (async () => {
await authenticate({ admin: true }); await authenticate({ admin: true });
await loadConfig(); await loadConfig();
const $t = get(t);
return { return {
meta: { meta: {
title: 'Onboarding', title: $t('onboarding'),
}, },
}; };
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@ -2,14 +2,14 @@
import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte'; import AdminRegistrationForm from '$lib/components/forms/admin-registration-form.svelte';
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { t } from 'svelte-i18n';
export let data: PageData; export let data: PageData;
</script> </script>
<FullscreenContainer title={data.meta.title}> <FullscreenContainer title={data.meta.title}>
<p slot="message"> <p slot="message">
Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative {$t('admin.registration_description')}
tasks, and additional users will be created by you.
</p> </p>
<AdminRegistrationForm /> <AdminRegistrationForm />

View File

@ -1,6 +1,8 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { getServerConfig } from '@immich/sdk'; import { getServerConfig } from '@immich/sdk';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async () => { export const load = (async () => {
@ -10,9 +12,11 @@ export const load = (async () => {
redirect(302, AppRoute.AUTH_LOGIN); redirect(302, AppRoute.AUTH_LOGIN);
} }
const $t = get(t);
return { return {
meta: { meta: {
title: 'Admin Registration', title: $t('admin.registration'),
}, },
}; };
}) satisfies PageLoad; }) satisfies PageLoad;