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:
parent
df9e074304
commit
dd2c7400a6
90 changed files with 635 additions and 322 deletions
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
})}
|
||||
|
|
|
@ -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}`);
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { albumViewSettings, SortOrder } from '$lib/stores/preferences.store';
|
||||
import { albumViewSettings, SortOrder, AlbumSortBy } from '$lib/stores/preferences.store';
|
||||
import type { AlbumSortOptionMetadata } from '$lib/utils/album-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let option: AlbumSortOptionMetadata;
|
||||
|
||||
|
@ -12,6 +13,17 @@
|
|||
$albumViewSettings.sortOrder = option.defaultOrder;
|
||||
}
|
||||
};
|
||||
|
||||
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
||||
return {
|
||||
[AlbumSortBy.Title]: $t('sort_title'),
|
||||
[AlbumSortBy.ItemCount]: $t('sort_items'),
|
||||
[AlbumSortBy.DateModified]: $t('sort_modified'),
|
||||
[AlbumSortBy.DateCreated]: $t('sort_created'),
|
||||
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
|
||||
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<th class="text-sm font-medium {option.columnStyle}">
|
||||
|
@ -27,6 +39,6 @@
|
|||
↑
|
||||
{/if}
|
||||
{/if}
|
||||
{option.text}
|
||||
{albumSortByNames[option.id]}
|
||||
</button>
|
||||
</th>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
disabled={selectedThumbnail == undefined}
|
||||
on:click={() => dispatch('thumbnail', selectedThumbnail)}
|
||||
>
|
||||
Done
|
||||
{$t('done')}
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
onRestore?.(ids);
|
||||
|
||||
notificationController.show({
|
||||
message: `Restored ${ids.length}`,
|
||||
message: $t('assets_restored_count', { values: { count: ids.length } }),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -228,7 +228,7 @@
|
|||
id={`${listboxId}-${0}`}
|
||||
on:click={() => closeDropdown()}
|
||||
>
|
||||
No results
|
||||
{$t('no_results')}
|
||||
</li>
|
||||
{/if}
|
||||
{#each filteredOptions as option, index (option.label)}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 } });
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue