mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00: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
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue