1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-16 00:36:47 +01:00

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

View file

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

View file

@ -9,6 +9,7 @@
import { mdiChevronRight } from '@mdi/js';
import AlbumCard from '$lib/components/album-page/album-card.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { t } from 'svelte-i18n';
export let albums: AlbumResponseDto[];
export let group: AlbumGroup | undefined = undefined;
@ -41,7 +42,7 @@
class="inline-block -mt-2.5 transition-all duration-[250ms] {iconRotation}"
/>
<span class="font-bold text-3xl text-black dark:text-white">{group.name}</span>
<span class="ml-1.5">({albums.length} {albums.length > 1 ? 'albums' : 'album'})</span>
<span class="ml-1.5">({$t('albums_count', { values: { count: albums.length } })})</span>
</button>
<hr class="dark:border-immich-dark-gray" />
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -304,7 +304,7 @@
const isConfirmed = await dialogController.show({
id: 'delete-album',
prompt: `Are you sure you want to delete the album ${albumToDelete.albumName}?\nIf this album is shared, other users will not be able to access it anymore.`,
prompt: $t('album_delete_confirmation', { values: { album: albumToDelete.albumName } }),
});
if (!isConfirmed) {
@ -340,7 +340,7 @@
message: $t('album_info_updated'),
type: NotificationType.Info,
button: {
text: 'View Album',
text: $t('view_album'),
onClick() {
return goto(`${AppRoute.ALBUMS}/${album.id}`);
},

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { albumViewSettings, SortOrder } from '$lib/stores/preferences.store';
import { albumViewSettings, SortOrder, AlbumSortBy } from '$lib/stores/preferences.store';
import type { AlbumSortOptionMetadata } from '$lib/utils/album-utils';
import { t } from 'svelte-i18n';
export let option: AlbumSortOptionMetadata;
@ -12,6 +13,17 @@
$albumViewSettings.sortOrder = option.defaultOrder;
}
};
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
return {
[AlbumSortBy.Title]: $t('sort_title'),
[AlbumSortBy.ItemCount]: $t('sort_items'),
[AlbumSortBy.DateModified]: $t('sort_modified'),
[AlbumSortBy.DateCreated]: $t('sort_created'),
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
};
})();
</script>
<th class="text-sm font-medium {option.columnStyle}">
@ -27,6 +39,6 @@
&#8593;
{/if}
{/if}
{option.text}
{albumSortByNames[option.id]}
</button>
</th>

View file

@ -34,7 +34,9 @@
path={mdiShareVariantOutline}
size="16"
class="inline ml-1 opacity-70"
title={album.ownerId === $user.id ? $t('shared_by_you') : `Shared by ${album.owner.name}`}
title={album.ownerId === $user.id
? $t('shared_by_you')
: $t('shared_by_user', { values: { user: album.owner.name } })}
/>
{/if}
</td>

View file

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

View file

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

View file

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

View file

@ -24,9 +24,9 @@
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
{ title: $t('editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
{ title: $t('viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
{ title: $t('remove'), value: 'none' },
{ title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
{ title: $t('role_viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
{ title: $t('remove_user'), value: 'none' },
];
const dispatch = createEventDispatcher<{
@ -110,7 +110,7 @@
{#if users.length + Object.keys(selectedUsers).length === 0}
<p class="p-5 text-sm">
Looks like you have shared this album with all users or you don't have any user to share with.
{$t('album_share_no_users')}
</p>
{/if}

View file

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

View file

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

View file

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

View file

@ -162,7 +162,7 @@
reactions = [...reactions, isLiked];
}
} catch (error) {
handleError(error, "Can't change favorite for asset");
handleError(error, $t('errors.unable_to_change_favorite'));
}
}
};
@ -189,7 +189,7 @@
const { comments } = await getActivityStatistics({ assetId: asset.id, albumId: album.id });
numberOfComments = comments;
} catch (error) {
handleError(error, "Can't get number of comments");
handleError(error, $t('errors.unable_to_get_comments_number'));
}
}
};
@ -395,10 +395,10 @@
notificationController.show({
type: NotificationType.Info,
message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`,
message: asset.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'),
});
} catch (error) {
handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: asset.isFavorite } }));
}
};
@ -429,7 +429,7 @@
notificationController.show({
type: NotificationType.Info,
message: `Restored asset`,
message: $t('restored_asset'),
});
} catch (error) {
handleError(error, $t('errors.unable_to_restore_assets'));
@ -446,9 +446,9 @@
const handleRunJob = async (name: AssetJobName) => {
try {
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) });
notificationController.show({ type: NotificationType.Info, message: $getAssetJobMessage(name) });
} catch (error) {
handleError(error, `Unable to submit job`);
handleError(error, $t('errors.unable_to_submit_job'));
}
};
@ -528,7 +528,7 @@
timeout: 1500,
});
} catch (error) {
handleError(error, 'Unable to update album cover');
handleError(error, $t('errors.unable_to_update_album_cover'));
}
};

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

View file

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

View file

@ -99,10 +99,10 @@
</div>
<div class="flex px-4 md:pt-4">
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
<h1 class="text-xl text-gray-500 dark:text-gray-300">{$t('are_these_the_same_person')}</h1>
</div>
<div class="flex px-4 pt-2">
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
</div>
<svelte:fragment slot="sticky-bottom">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button>

View file

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

View file

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

View file

@ -24,7 +24,7 @@
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg">
Date of birth is used to calculate the age of this person at the time of a photo.
{$t('birthdate_set_description')}
</p>
</div>

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@
<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg">
This value will only be shown once. Please be sure to copy it before closing the window.
{$t('api_key_description')}
</p>
</div>

View file

@ -57,6 +57,6 @@
<p class="text-sm text-immich-primary">{success}</p>
{/if}
<div class="my-5 flex w-full">
<Button type="submit" size="lg" fullwidth>{$t('change_password')}</Button>
<Button type="submit" size="lg" fullwidth>{$t('to_change_password')}</Button>
</div>
</form>

View file

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

View file

@ -36,7 +36,7 @@
return;
} catch (error) {
console.error('Error [login-form] [oauth.callback]', error);
oauthError = getServerErrorMessage(error) || 'Unable to complete OAuth login';
oauthError = getServerErrorMessage(error) || $t('errors.unable_to_complete_oauth_login');
oauthLoading = false;
}
}
@ -48,7 +48,7 @@
return;
}
} catch (error) {
handleError(error, 'Unable to connect!');
handleError(error, $t('errors.unable_to_connect'));
}
oauthLoading = false;
@ -74,7 +74,7 @@
await onSuccess();
return;
} catch (error) {
errorMessage = getServerErrorMessage(error) || 'Incorrect email or password';
errorMessage = getServerErrorMessage(error) || $t('errors.incorrect_email_or_password');
loading = false;
return;
}
@ -86,7 +86,7 @@
const success = await oauth.authorize(window.location);
if (!success) {
oauthLoading = false;
oauthError = 'Unable to login with OAuth';
oauthError = $t('errors.unable_to_login_with_oauth');
}
};
</script>
@ -124,7 +124,7 @@
<LoadingSpinner />
</span>
{:else}
Login
{$t('to_login')}
{/if}
</Button>
</div>
@ -138,7 +138,7 @@
<span
class="absolute left-1/2 -translate-x-1/2 bg-white px-3 font-medium text-gray-900 dark:bg-immich-dark-gray dark:text-white"
>
or
{$t('or')}
</span>
</div>
{/if}

View file

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

View file

@ -16,9 +16,9 @@
<OnboardingCard>
<ImmichLogo noText width="75" />
<p class="font-medium text-6xl my-6 text-immich-primary dark:text-immich-dark-primary">
Welcome, {$user.name}
{$t('onboarding_welcome_user', { values: { user: $user.name } })}
</p>
<p class="text-3xl pb-6 font-light">Let's get your instance set up with some common settings.</p>
<p class="text-3xl pb-6 font-light">{$t('onboarding_welcome_description')}</p>
<div class="w-full flex place-content-end">
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('done')}>

View file

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

View file

@ -19,7 +19,7 @@
<p class="text-xl text-immich-primary dark:text-immich-dark-primary">{$t('color_theme').toUpperCase()}</p>
<div>
<p class="pb-6 font-light">Choose a color theme for your instance. You can change this later in your settings.</p>
<p class="pb-6 font-light">{$t('onboarding_theme_description')}</p>
</div>
<div class="flex gap-4 mb-6">

View file

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

View file

@ -44,13 +44,15 @@
onFavorite(ids, isFavorite);
notificationController.show({
message: isFavorite ? `Added ${ids.length} to favorites` : `Removed ${ids.length} from favorites`,
message: isFavorite
? $t('added_to_favorites_count', { values: { count: ids.length } })
: $t('removed_from_favorites_count', { values: { count: ids.length } }),
type: NotificationType.Info,
});
clearSelect();
} catch (error) {
handleError(error, `Unable to ${isFavorite ? 'add to' : 'remove from'} favorites`);
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: isFavorite } }));
} finally {
loading = false;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import Checkbox from '$lib/components/elements/checkbox.svelte';
import { s } from '$lib/utils';
import { t } from 'svelte-i18n';
export let size: number;
@ -24,7 +23,7 @@
</script>
<ConfirmDialog
title="Permanently delete asset{s(size)}"
title={$t('permanently_delete_assets_count', { values: { count: size } })}
confirmText={$t('delete')}
onConfirm={handleConfirm}
onCancel={() => dispatch('cancel')}
@ -38,10 +37,10 @@
this asset? This will also remove it from its album(s).
{/if}
</p>
<p><b>You cannot undo this action!</b></p>
<p><b>{$t('cannot_undo_this_action')}</b></p>
<div class="pt-4 flex justify-center items-center">
<Checkbox id="confirm-deletion-input" label="Do not show this message again" bind:checked />
<Checkbox id="confirm-deletion-input" label={$t('do_not_show_again')} bind:checked />
</div>
</svelte:fragment>
</ConfirmDialog>

View file

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

View file

@ -99,17 +99,16 @@
{#if !shared}
<p class="px-5 py-3 text-xs">
{#if search.length === 0}ALL
{/if}ALBUMS
{(search.length === 0 ? $t('all_albums') : $t('albums')).toUpperCase()}
</p>
{/if}
{#each filteredAlbums as album (album.id)}
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} />
{/each}
{:else if albums.length > 0}
<p class="px-5 py-1 text-sm">It looks like you do not have any albums with this name yet.</p>
<p class="px-5 py-1 text-sm">{$t('no_albums_with_name_yet')}</p>
{:else}
<p class="px-5 py-1 text-sm">It looks like you do not have any albums yet.</p>
<p class="px-5 py-1 text-sm">{$t('no_albums_yet')}</p>
{/if}
</div>
{/if}

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
import { t } from 'svelte-i18n';
export let title = $t('confirm');
export let prompt = 'Are you sure you want to do this?';
export let prompt = $t('are_you_sure_to_do_this');
export let confirmText = $t('confirm');
export let confirmColor: Color = 'red';
export let cancelText = $t('cancel');

View file

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

View file

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

View file

@ -187,7 +187,9 @@
src={getAssetThumbnailUrl(feature.properties?.id)}
class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary"
alt={feature.properties?.city && feature.properties.country
? `Map marker for images taken in ${feature.properties.city}, ${feature.properties.country}`
? $t('map_marker_for_images', {
values: { city: feature.properties.city, country: feature.properties.country },
})
: $t('map_marker_with_image')}
/>
{/if}

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');
});
});

View file

@ -77,7 +77,9 @@
<div class="flex place-items-center gap-2">
<Icon path={icon} color={primaryColor[notification.type]} size="20" />
<h2 style:color={primaryColor[notification.type]} class="font-medium" data-testid="title">
{notification.type.toString()}
{#if notification.type == NotificationType.Error}{$t('error')}
{:else if notification.type == NotificationType.Warning}{$t('warning')}
{:else if notification.type == NotificationType.Info}{$t('info')}{/if}
</h2>
</div>
<CircleIconButton

View file

@ -50,7 +50,7 @@
if (await hasTransparentPixels(blob)) {
notificationController.show({
type: NotificationType.Error,
message: 'Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.',
message: $t('errors.profile_picture_transparent_pixels'),
timeout: 3000,
});
return;

View file

@ -19,7 +19,7 @@
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
<Checkbox id="not-in-album-checkbox" label={$t('not_in_any_album')} bind:checked={filters.isNotInAlbum} />
<Checkbox id="archive-checkbox" label={$t('archive')} bind:checked={filters.isArchive} />
<Checkbox id="favorite-checkbox" label={$t('favorite')} bind:checked={filters.isFavorite} />
<Checkbox id="favorite-checkbox" label={$t('favorites')} bind:checked={filters.isFavorite} />
</div>
</fieldset>
</div>

View file

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

View file

@ -21,7 +21,7 @@
on:click={() => dispatch('reset', { default: true })}
class="bg-none text-sm font-medium text-immich-primary hover:text-immich-primary/75 dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75"
>
Reset to default
{$t('reset_to_default')}
</button>
{/if}
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@
const shortcuts: Shortcuts = {
general: [
{ key: ['←', '→'], action: $t('previous_or_next_photo') },
{ key: ['Esc'], action: 'Back, close, or deselect' },
{ key: ['Esc'], action: $t('back_close_deselect') },
{ key: ['Ctrl', 'k'], action: $t('search_your_photos') },
{ key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') },
],
@ -30,7 +30,7 @@
{ key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') },
{ key: ['⇧', 'd'], action: $t('download') },
{ key: ['Space'], action: $t('play_or_pause_video') },
{ key: ['Del'], action: 'Trash/Delete Asset', info: 'press ⇧ to permanently delete asset' },
{ key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') },
],
};
const dispatch = createEventDispatcher<{

View file

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

View file

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

View file

@ -30,13 +30,13 @@
expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject();
if (expirationCountdown.days && expirationCountdown.days > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'days' });
} else if (expirationCountdown.hours && expirationCountdown.hours > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'hours' });
return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'hours' });
} else if (expirationCountdown.minutes && expirationCountdown.minutes > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'minutes' });
return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'minutes' });
} else if (expirationCountdown.seconds && expirationCountdown.seconds > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'seconds' });
return expiresAtDate.toRelativeCalendar({ base: now, locale: $locale, unit: 'seconds' });
}
};
@ -63,11 +63,11 @@
<p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p>
{:else}
<p>
Expires {getCountDownExpirationDate()}
{$t('expires_date', { values: { date: getCountDownExpirationDate() } })}
</p>
{/if}
{:else}
<p>Expires ∞</p>
<p>{$t('expires_date', { values: { date: '∞' } })}</p>
{/if}
</div>
@ -97,7 +97,7 @@
<div
class="flex w-[80px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
>
Upload
{$t('upload')}
</div>
{/if}
@ -105,7 +105,7 @@
<div
class="flex w-[100px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
>
Download
{$t('download')}
</div>
{/if}
@ -113,7 +113,7 @@
<div
class="flex w-[60px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
>
EXIF
{$t('exif').toUpperCase()}
</div>
{/if}
@ -121,7 +121,7 @@
<div
class="flex w-[100px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
>
Password
{$t('password')}
</div>
{/if}
</div>

View file

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

View file

@ -32,9 +32,9 @@
$preferences.emailNotifications.albumInvite = data.emailNotifications.albumInvite;
$preferences.emailNotifications.albumUpdate = data.emailNotifications.albumUpdate;
notificationController.show({ message: 'Saved settings', type: NotificationType.Info });
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
} catch (error) {
handleError(error, 'Unable to update settings');
handleError(error, $t('errors.unable_to_update_settings'));
}
};
</script>

View file

@ -63,7 +63,7 @@
{/each}
{:else}
<p class="py-5 text-sm">
Looks like you shared your photos with all users or you don't have any user to share with.
{$t('photo_shared_all_users')}
</p>
{/if}

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,4 +1,6 @@
import type { PersonResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
export const searchNameLocal = (
name: string,
@ -26,5 +28,6 @@ export const searchNameLocal = (
};
export const getPersonNameWithHiddenValue = (name: string, isHidden: boolean) => {
return `${name ? name + (isHidden ? ' ' : '') : ''}${isHidden ? '(hidden)' : ''}`;
const $t = get(t);
return $t('person_hidden', { values: { name: name, hidden: isHidden } });
};

View file

@ -37,10 +37,9 @@
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
import { handlePromiseError, s } from '$lib/utils';
import { handlePromiseError } from '$lib/utils';
import { downloadAlbum } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
@ -168,10 +167,10 @@
});
notificationController.show({
type: NotificationType.Info,
message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`,
message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }),
});
} catch (error) {
handleError(error, `Can't ${album.isActivityEnabled ? 'disable' : 'enable'} activity`);
handleError(error, $t('errors.cant_change_activity', { values: { enabled: album.isActivityEnabled } }));
}
};
@ -189,7 +188,7 @@
reactions = [...reactions, isLiked];
}
} catch (error) {
handleError(error, "Can't change favorite for asset");
handleError(error, $t('errors.cant_change_asset_favorite'));
}
};
@ -216,7 +215,7 @@
const { comments } = await getActivityStatistics({ albumId: album.id });
setNumberOfComments(comments);
} catch (error) {
handleError(error, "Can't get number of comments");
handleError(error, $t('errors.cant_get_number_of_comments'));
}
};
@ -283,7 +282,7 @@
const count = results.filter(({ success }) => success).length;
notificationController.show({
type: NotificationType.Info,
message: `Added ${count} asset${s(count)}`,
message: $t('assets_added_count', { values: { count: count } }),
});
await refreshAlbum();
@ -291,7 +290,7 @@
timelineInteractionStore.clearMultiselect();
viewMode = ViewMode.VIEW;
} catch (error) {
handleError(error, 'Error adding assets to album');
handleError(error, $t('errors.error_adding_assets_to_album'));
}
};
@ -317,7 +316,7 @@
viewMode = ViewMode.VIEW;
} catch (error) {
handleError(error, 'Error adding users to album');
handleError(error, $t('errors.error_adding_users_to_album'));
}
};
@ -331,7 +330,7 @@
await refreshAlbum();
viewMode = album.albumUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW;
} catch (error) {
handleError(error, $t('errors.unable_to_load_album'));
handleError(error, $t('errors.error_deleting_shared_user'));
}
};
@ -342,7 +341,7 @@
const handleRemoveAlbum = async () => {
const isConfirmed = await dialogController.show({
id: 'remove-album',
prompt: `Are you sure you want to delete the album ${album.albumName}?\nIf this album is shared, other users will not be able to access it anymore.`,
prompt: $t('album_delete_confirmation', { values: { album: album.albumName } }),
});
if (!isConfirmed) {
@ -393,7 +392,7 @@
},
});
} catch (error) {
handleError(error, 'Unable to update album cover');
handleError(error, $t('errors.unable_to_update_album_cover'));
}
};
@ -495,9 +494,9 @@
<svelte:fragment slot="leading">
<p class="text-lg dark:text-immich-dark-fg">
{#if $timelineSelected.size === 0}
Add to album
{$t('add_to_album')}
{:else}
{$timelineSelected.size.toLocaleString($locale)} selected
{$t('selected_count', { values: { count: $timelineSelected.size } })}
{/if}
</p>
</svelte:fragment>
@ -508,7 +507,7 @@
on:click={handleSelectFromComputer}
class="rounded-lg px-6 py-2 text-sm font-medium text-immich-primary transition-all hover:bg-immich-primary/10 dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/25"
>
Select from computer
{$t('select_from_computer')}
</button>
<Button size="sm" rounded="lg" disabled={$timelineSelected.size === 0} on:click={handleAddAssets}
>{$t('done')}</Button

View file

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

View file

@ -30,7 +30,7 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
import { websocketEvents } from '$lib/stores/websocket';
import { getPeopleThumbnailUrl, handlePromiseError, s } from '$lib/utils';
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
import { clickOutside } from '$lib/actions/click-outside';
import { handleError } from '$lib/utils/handle-error';
import { isExternalUrl } from '$lib/utils/navigation';
@ -488,7 +488,7 @@
{#if data.person.name}
<p class="w-40 sm:w-72 font-medium truncate">{data.person.name}</p>
<p class="absolute w-fit text-sm text-gray-500 dark:text-immich-gray bottom-0">
{`${numberOfAssets} asset${s(numberOfAssets)}`}
{$t('assets_count', { values: { count: numberOfAssets } })}
</p>
{:else}
<p class="font-medium">{$t('add_a_name')}</p>

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>
<AlbumCardGroup albums={searchResultAlbums} showDateRange showItemCount />
<div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">PHOTOS & VIDEOS</div>
<div class="m-6 text-4xl font-medium text-black/70 dark:text-white/80">
{$t('photos_and_videos').toUpperCase()}
</div>
</section>
{/if}
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
@ -296,7 +298,7 @@
<div class="flex flex-col content-center items-center text-center">
<Icon path={mdiImageOffOutline} size="3.5em" />
<p class="mt-5 text-3xl font-medium">{$t('no_results')}</p>
<p class="text-base font-normal">Try a synonym or more general keyword</p>
<p class="text-base font-normal">{$t('no_results_description')}</p>
</div>
</div>
{/if}

View file

@ -37,8 +37,7 @@
const handleEmptyTrash = async () => {
const isConfirmed = await dialogController.show({
id: 'empty-trash',
prompt:
'Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!',
prompt: $t('empty_trash_confirmation'),
});
if (!isConfirmed) {
@ -53,7 +52,7 @@
assetStore.removeAssets(deletedAssetIds);
notificationController.show({
message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }),
type: NotificationType.Info,
});
} catch (error) {
@ -64,7 +63,7 @@
const handleRestoreTrash = async () => {
const isConfirmed = await dialogController.show({
id: 'restore-trash',
prompt: 'Are you sure you want to restore all your trashed assets? You cannot undo this action!',
prompt: $t('assets_restore_confirmation'),
});
if (!isConfirmed) {
@ -78,7 +77,7 @@
assetStore.removeAssets(restoredAssetIds);
notificationController.show({
message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
message: $t('assets_restored_count', { values: { count: numberOfAssets } }),
type: NotificationType.Info,
});
} catch (error) {

View file

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

View file

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

View file

@ -6,6 +6,7 @@
import { resetSavedUser, user } from '$lib/stores/user.store';
import { logout } from '@immich/sdk';
import type { PageData } from './$types';
import { t } from 'svelte-i18n';
export let data: PageData;
@ -18,11 +19,10 @@
<FullscreenContainer title={data.meta.title}>
<p slot="message">
Hi {$user.name} ({$user.email}),
{$t('hi_user', { values: { name: $user.name, email: $user.email } })}
<br />
<br />
This is either the first time you are signing into the system or a request has been made to change your password. Please
enter the new password below.
{$t('change_password_description')}
</p>
<ChangePasswordForm on:success={onSuccess} />

View file

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

View file

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

View file

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

View file

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

View file

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