chore(web): another missing translations (#10274)

* chore(web): another missing translations

* unused removed

* more keys

* lint fix

* test fixed

* dynamic translation fix

* fixes

* people search translation

* params fixed

* keep filter setting fix

* lint fix

* $t fixes

* Update web/src/lib/i18n/en.json

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* another missing

* activity translation

* link sharing translations

* expiration dropdown fix - didn't work localized

* notification title

* device logout

* search results

* reset to default

* unsaved change

* select from computer

* selected

* select-2

* select-3

* unmerge

* pluralize, force icu message

* Update web/src/lib/components/asset-viewer/asset-viewer.svelte

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* review fixes

* remove user

* plural fixes

* ffmpeg settings

* fixes

* error title

* plural fixes

* onboarding

* change password

* more more

* console log fix

* another

* api key desc

* map marker

* format fix

* key fix

* asset-utils

* utils

* misc

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
This commit is contained in:
waclaw66 2024-06-24 15:50:01 +02:00 committed by GitHub
parent df9e074304
commit dd2c7400a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 635 additions and 322 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ describe('NotificationCard component', () => {
},
});
expect(sut.getByTestId('title')).toHaveTextContent('Info');
expect(sut.getByTestId('title')).toHaveTextContent('info');
expect(sut.getByTestId('message')).toHaveTextContent('Notification message');
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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