mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(web): translations (#9854)
* First test * Added translation using Weblate (French) * Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/fr/ * Further testing * Further testing * Translated using Weblate (German) Currently translated at 100.0% (18 of 18 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Further work * Update string file. * More strings * Automatically changed strings * Add automatically translated german file for testing purposes * Fix merge-face-selector component * Make server stats strings uppercase * Fix uppercase string * Fix some strings in jobs-panel * Fix lower and uppercase strings. Add a few additional string. Fix a few unnecessary replacements * Update german test translations * Fix typo in locales file * Change string keys * Extract more strings * Extract and replace some more strings * Update testtranslationfile * Change translation keys * Fix rebase errors * Fix one more rebase error * Remove german translation file * Co-authored-by: Daniel Dietzler <danieldietzler@users.noreply.github.com> * chore: clean up translations * chore: add new line * fix formatting * chore: fixes * fix: loading and tests --------- Co-authored-by: root <root@Blacki> Co-authored-by: admin <admin@example.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
parent
a2bccf23c9
commit
f446bc8caa
177 changed files with 2779 additions and 1017 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-US.json'));
|
||||
await waitLocale('en-US');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
album: albumFactory.build({ albumThumbnailAssetId: null, shared: false, assetCount: 0 }),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
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;
|
||||
export let showOwner = false;
|
||||
|
|
@ -35,7 +36,7 @@
|
|||
>
|
||||
<CircleIconButton
|
||||
color="opaque"
|
||||
title="Show album options"
|
||||
title={$t('show_album_options')}
|
||||
icon={mdiDotsVertical}
|
||||
size="20"
|
||||
padding="2"
|
||||
|
|
@ -76,14 +77,14 @@
|
|||
|
||||
{#if showOwner}
|
||||
{#if $user.id === album.ownerId}
|
||||
<p>Owned</p>
|
||||
<p>{$t('owned')}</p>
|
||||
{:else if album.owner}
|
||||
<p>Shared by {album.owner.name}</p>
|
||||
{:else}
|
||||
<p>Shared</p>
|
||||
<p>{$t('shared')}</p>
|
||||
{/if}
|
||||
{:else if album.shared}
|
||||
<p>Shared</p>
|
||||
<p>{$t('shared')}</p>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto | undefined;
|
||||
export let preload = false;
|
||||
|
|
@ -15,7 +16,7 @@
|
|||
<img
|
||||
loading={preload ? 'eager' : 'lazy'}
|
||||
src={thumbnailUrl}
|
||||
alt={album?.albumName ?? 'Unknown Album'}
|
||||
alt={album?.albumName ?? $t('unknown_album')}
|
||||
class="z-0 rounded-xl object-cover {css}"
|
||||
data-testid="album-image"
|
||||
draggable="false"
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
loading={preload ? 'eager' : 'lazy'}
|
||||
src="$lib/assets/no-thumbnail.png"
|
||||
sizes="min(271px,186px)"
|
||||
alt={album?.albumName ?? 'Empty Album'}
|
||||
alt={album?.albumName ?? $t('empty_album')}
|
||||
class="z-0 rounded-xl object-cover {css}"
|
||||
data-testid="album-image"
|
||||
draggable="false"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { init } from 'svelte-i18n';
|
||||
import { describe } from 'vitest';
|
||||
|
||||
describe('AlbumDescription component', () => {
|
||||
beforeAll(async () => {
|
||||
await init({ fallbackLocale: 'en-US' });
|
||||
});
|
||||
|
||||
it('shows an AutogrowTextarea component when isOwned is true', () => {
|
||||
render(AlbumDescription, { isOwned: true, id: '', description: '' });
|
||||
const autogrowTextarea = screen.getByTestId('autogrow-textarea');
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { updateAlbumInfo } from '@immich/sdk';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let id: string;
|
||||
export let description: string;
|
||||
|
|
@ -16,7 +17,7 @@
|
|||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Error updating album description');
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
}
|
||||
description = newDescription;
|
||||
};
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
content={description}
|
||||
class="w-full mt-2 text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
|
||||
onContentUpdate={handleUpdateDescription}
|
||||
placeholder="Add a description"
|
||||
placeholder={$t('add_a_description')}
|
||||
/>
|
||||
{:else if description}
|
||||
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import type { RenderedOption } from '../elements/dropdown.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { findKey } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let order: AssetOrder | undefined;
|
||||
|
|
@ -17,8 +18,8 @@
|
|||
export let onChangeOrder: (order: AssetOrder) => void;
|
||||
|
||||
const options: Record<AssetOrder, RenderedOption> = {
|
||||
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: 'Oldest first' },
|
||||
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: 'Newest first' },
|
||||
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
|
||||
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
|
||||
};
|
||||
|
||||
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
|
||||
|
|
@ -45,19 +46,19 @@
|
|||
});
|
||||
onChangeOrder(order);
|
||||
} catch (error) {
|
||||
handleError(error, 'Error updating album order');
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal title="Options" onClose={() => dispatch('close')}>
|
||||
<FullScreenModal title={$t('options')} onClose={() => dispatch('close')}>
|
||||
<div class="items-center justify-center">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
|
||||
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
|
||||
<div class="grid p-2 gap-y-2">
|
||||
{#if order}
|
||||
<SettingDropdown
|
||||
title="Display order"
|
||||
title={$t('display_order')}
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[order]}
|
||||
onToggle={handleToggle}
|
||||
|
|
@ -65,27 +66,27 @@
|
|||
{/if}
|
||||
<SettingSwitch
|
||||
title="Comments & likes"
|
||||
subtitle="Let others respond"
|
||||
subtitle={$t('let_others_respond')}
|
||||
checked={album.isActivityEnabled}
|
||||
on:toggle={() => dispatch('toggleEnableActivity')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
||||
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
|
||||
<div class="p-2">
|
||||
<button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
<div>Invite People</div>
|
||||
<div>{$t('invite_people')}</div>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 py-2 mt-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
<div>Owner</div>
|
||||
<div>{$t('owner')}</div>
|
||||
</div>
|
||||
{#each album.albumUsers as { user } (user.id)}
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { updateAlbumInfo } from '@immich/sdk';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let id: string;
|
||||
export let albumName: string;
|
||||
|
|
@ -22,7 +23,7 @@
|
|||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to update album name');
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
return;
|
||||
}
|
||||
albumName = newAlbumName;
|
||||
|
|
@ -38,6 +39,6 @@
|
|||
type="text"
|
||||
bind:value={newAlbumName}
|
||||
disabled={!isOwned}
|
||||
title="Edit Title"
|
||||
placeholder="Add a title"
|
||||
title={$t('edit_title')}
|
||||
placeholder={$t('add_a_title')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import AlbumSummary from './album-summary.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
|
@ -72,14 +73,18 @@
|
|||
<svelte:fragment slot="trailing">
|
||||
{#if sharedLink.allowUpload}
|
||||
<CircleIconButton
|
||||
title="Add Photos"
|
||||
title={$t('add_photos')}
|
||||
on:click={() => openFileUploadDialog({ albumId: album.id })}
|
||||
icon={mdiFileImagePlusOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount > 0 && sharedLink.allowDownload}
|
||||
<CircleIconButton title="Download" on:click={() => downloadAlbum(album)} icon={mdiFolderDownloadOutline} />
|
||||
<CircleIconButton
|
||||
title={$t('download')}
|
||||
on:click={() => downloadAlbum(album)}
|
||||
icon={mdiFolderDownloadOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ThemeButton />
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
import GroupTab from '$lib/components/elements/group-tab.svelte';
|
||||
import { createAlbumAndRedirect, collapseAllAlbumGroups, expandAllAlbumGroups } from '$lib/utils/album-utils';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let albumGroups: string[];
|
||||
export let searchQuery: string;
|
||||
|
|
@ -100,20 +101,20 @@
|
|||
|
||||
<!-- Search Albums -->
|
||||
<div class="hidden xl:block h-10 xl:w-60 2xl:w-80">
|
||||
<SearchBar placeholder="Search albums" bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
<SearchBar placeholder={$t('search_albums')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
|
||||
<!-- Create Album -->
|
||||
<LinkButton on:click={() => createAlbumAndRedirect()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiPlusBoxOutline} size="18" />
|
||||
<p class="hidden md:block">Create album</p>
|
||||
<p class="hidden md:block">{$t('create_album')}</p>
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
||||
<!-- Sort Albums -->
|
||||
<Dropdown
|
||||
title="Sort albums by..."
|
||||
title={$t('sort_albums_by')}
|
||||
options={Object.values(sortOptionsMetadata)}
|
||||
selectedOption={selectedSortOption}
|
||||
on:select={({ detail }) => handleChangeSortBy(detail)}
|
||||
|
|
@ -125,7 +126,7 @@
|
|||
|
||||
<!-- Group Albums -->
|
||||
<Dropdown
|
||||
title="Group albums by..."
|
||||
title={$t('group_albums_by')}
|
||||
options={Object.values(groupOptionsMetadata)}
|
||||
selectedOption={selectedGroupOption}
|
||||
on:select={({ detail }) => handleChangeGroupBy(detail)}
|
||||
|
|
@ -141,7 +142,7 @@
|
|||
<!-- Expand Album Groups -->
|
||||
<div class="hidden xl:flex gap-0">
|
||||
<div class="block">
|
||||
<LinkButton title="Expand all" on:click={() => expandAllAlbumGroups()}>
|
||||
<LinkButton title={$t('expand_all')} on:click={() => expandAllAlbumGroups()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiUnfoldMoreHorizontal} size="18" />
|
||||
</div>
|
||||
|
|
@ -150,7 +151,7 @@
|
|||
|
||||
<!-- Collapse Album Groups -->
|
||||
<div class="block">
|
||||
<LinkButton title="Collapse all" on:click={() => collapseAllAlbumGroups(albumGroups)}>
|
||||
<LinkButton title={$t('collapse_all')} on:click={() => collapseAllAlbumGroups(albumGroups)}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiUnfoldLessHorizontal} size="18" />
|
||||
</div>
|
||||
|
|
@ -165,10 +166,10 @@
|
|||
<div class="flex place-items-center gap-2 text-sm">
|
||||
{#if $albumViewSettings.view === AlbumViewMode.List}
|
||||
<Icon path={mdiViewGridOutline} size="18" />
|
||||
<p class="hidden md:block">Covers</p>
|
||||
<p class="hidden md:block">{$t('covers')}</p>
|
||||
{:else}
|
||||
<Icon path={mdiFormatListBulletedSquare} size="18" />
|
||||
<p class="hidden md:block">List</p>
|
||||
<p class="hidden md:block">{$t('list')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</LinkButton>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let ownedAlbums: AlbumResponseDto[] = [];
|
||||
export let sharedAlbums: AlbumResponseDto[] = [];
|
||||
|
|
@ -55,8 +56,8 @@
|
|||
[AlbumGroupBy.None]: (order, albums): AlbumGroup[] => {
|
||||
return [
|
||||
{
|
||||
id: 'Albums',
|
||||
name: 'Albums',
|
||||
id: $t('albums'),
|
||||
name: $t('albums'),
|
||||
albums,
|
||||
},
|
||||
];
|
||||
|
|
@ -64,7 +65,7 @@
|
|||
|
||||
/** Group by year */
|
||||
[AlbumGroupBy.Year]: (order, albums): AlbumGroup[] => {
|
||||
const unknownYear = 'Unknown Year';
|
||||
const unknownYear = $t('unknown_year');
|
||||
const useStartDate = userSettings.sortBy === AlbumSortBy.OldestPhoto;
|
||||
|
||||
const groupedByYear = groupBy(albums, (album) => {
|
||||
|
|
@ -111,7 +112,7 @@
|
|||
|
||||
return sortedByOwnerNames.map(([ownerId, albums]) => ({
|
||||
id: ownerId,
|
||||
name: ownerId === currentUserId ? 'My albums' : albums[0].owner.name,
|
||||
name: ownerId === currentUserId ? $t('my_albums') : albums[0].owner.name,
|
||||
albums,
|
||||
}));
|
||||
},
|
||||
|
|
@ -314,7 +315,7 @@
|
|||
await handleDeleteAlbum(albumToDelete);
|
||||
} catch {
|
||||
notificationController.show({
|
||||
message: 'Error deleting album',
|
||||
message: $t('errors.errors.unable_to_delete_album'),
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -336,7 +337,7 @@
|
|||
albumToEdit = null;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Album info updated',
|
||||
message: $t('album_info_updated'),
|
||||
type: NotificationType.Info,
|
||||
button: {
|
||||
text: 'View Album',
|
||||
|
|
@ -362,7 +363,7 @@
|
|||
});
|
||||
updateAlbumInfo(album);
|
||||
} catch (error) {
|
||||
handleError(error, 'Error adding users to album');
|
||||
handleError(error, $t('errors.unable_to_add_album_users'));
|
||||
} finally {
|
||||
albumToShare = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { mdiShareVariantOutline } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
path={mdiShareVariantOutline}
|
||||
size="16"
|
||||
class="inline ml-1 opacity-70"
|
||||
title={album.ownerId === $user.id ? 'Shared by you' : `Shared by ${album.owner.name}`}
|
||||
title={album.ownerId === $user.id ? $t('shared_by_you') : `Shared by ${album.owner.name}`}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let onClose: () => void;
|
||||
|
|
@ -38,7 +39,7 @@
|
|||
try {
|
||||
currentUser = await getMyUser();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to refresh user');
|
||||
handleError(error, $t('errors.unable_to_refresh_user'));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -66,7 +67,7 @@
|
|||
const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`;
|
||||
notificationController.show({ type: NotificationType.Info, message });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove user');
|
||||
handleError(error, $t('errors.unable_to_remove_album_users'));
|
||||
} finally {
|
||||
selectedRemoveUser = null;
|
||||
}
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
dispatch('refreshAlbum');
|
||||
notificationController.show({ type: NotificationType.Info, message });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to set user role');
|
||||
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
||||
} finally {
|
||||
selectedRemoveUser = null;
|
||||
}
|
||||
|
|
@ -87,7 +88,7 @@
|
|||
</script>
|
||||
|
||||
{#if !selectedRemoveUser}
|
||||
<FullScreenModal title="Options" {onClose}>
|
||||
<FullScreenModal title={$t('options')} {onClose}>
|
||||
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
|
||||
<div class="flex w-full place-items-center justify-between gap-4 p-5">
|
||||
<div class="flex place-items-center gap-4">
|
||||
|
|
@ -96,7 +97,7 @@
|
|||
</div>
|
||||
|
||||
<div id="icon-{album.owner.id}" class="flex place-items-center">
|
||||
<p class="text-sm">Owner</p>
|
||||
<p class="text-sm">{$t('owner')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{#each album.albumUsers as { user, role }}
|
||||
|
|
@ -119,7 +120,7 @@
|
|||
{#if isOwned}
|
||||
<div>
|
||||
<CircleIconButton
|
||||
title="Options"
|
||||
title={$t('options')}
|
||||
on:click={(event) => showContextMenu(event, user)}
|
||||
icon={mdiDotsVertical}
|
||||
size="20"
|
||||
|
|
@ -128,14 +129,17 @@
|
|||
{#if selectedMenuUser === user}
|
||||
<ContextMenu {...position} onClose={() => (selectedMenuUser = null)}>
|
||||
{#if role === AlbumUserRole.Viewer}
|
||||
<MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)} text="Allow edits" />
|
||||
<MenuOption
|
||||
on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)}
|
||||
text={$t('allow_edits')}
|
||||
/>
|
||||
{:else}
|
||||
<MenuOption
|
||||
on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)}
|
||||
text="Disallow edits"
|
||||
text={$t('disallow_edits')}
|
||||
/>
|
||||
{/if}
|
||||
<MenuOption on:click={handleMenuRemove} text="Remove" />
|
||||
<MenuOption on:click={handleMenuRemove} text={$t('remove')} />
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -144,7 +148,7 @@
|
|||
type="button"
|
||||
on:click={() => (selectedRemoveUser = user)}
|
||||
class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
|
||||
>Leave</button
|
||||
>{$t('leave')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -158,7 +162,7 @@
|
|||
<ConfirmDialog
|
||||
title="Leave album?"
|
||||
prompt="Are you sure you want to leave {album.albumName}?"
|
||||
confirmText="Leave"
|
||||
confirmText={$t('leave')}
|
||||
onConfirm={handleRemoveUser}
|
||||
onCancel={() => (selectedRemoveUser = null)}
|
||||
/>
|
||||
|
|
@ -168,7 +172,7 @@
|
|||
<ConfirmDialog
|
||||
title="Remove user?"
|
||||
prompt="Are you sure you want to remove {selectedRemoveUser.name}?"
|
||||
confirmText="Remove"
|
||||
confirmText={$t('remove')}
|
||||
onConfirm={handleRemoveUser}
|
||||
onCancel={() => (selectedRemoveUser = null)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
>
|
||||
<ControlAppBar on:close={() => dispatch('close')}>
|
||||
<svelte:fragment slot="leading">
|
||||
<p class="text-lg">Select album cover</p>
|
||||
<p class="text-lg">{$t('select_album_cover')}</p>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let onClose: () => void;
|
||||
|
|
@ -23,9 +24,9 @@
|
|||
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
|
||||
|
||||
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
||||
{ title: 'Editor', value: AlbumUserRole.Editor, icon: mdiPencil },
|
||||
{ title: 'Viewer', value: AlbumUserRole.Viewer, icon: mdiEye },
|
||||
{ title: 'Remove', value: 'none' },
|
||||
{ title: $t('editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
|
||||
{ title: $t('viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
|
||||
{ title: $t('remove'), value: 'none' },
|
||||
];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
|
|
@ -70,10 +71,10 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal title="Invite to album" showLogo {onClose}>
|
||||
<FullScreenModal title={$t('invite_to_album')} showLogo {onClose}>
|
||||
{#if Object.keys(selectedUsers).length > 0}
|
||||
<div class="mb-2 py-2 sticky">
|
||||
<p class="text-xs font-medium">SELECTED</p>
|
||||
<p class="text-xs font-medium">{$t('selected').toUpperCase()}</p>
|
||||
<div class="my-2">
|
||||
{#each Object.values(selectedUsers) as { user }}
|
||||
{#key user.id}
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
</div>
|
||||
|
||||
<Dropdown
|
||||
title="Role"
|
||||
title={$t('role')}
|
||||
options={roleOptions}
|
||||
render={({ title, icon }) => ({ title, icon })}
|
||||
on:select={({ detail: { value } }) => handleChangeRole(user, value)}
|
||||
|
|
@ -115,7 +116,7 @@
|
|||
|
||||
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
|
||||
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
|
||||
<p class="text-xs font-medium">SUGGESTIONS</p>
|
||||
<p class="text-xs font-medium">{$t('suggestions').toUpperCase()}</p>
|
||||
|
||||
<div class="my-2">
|
||||
{#each users as user}
|
||||
|
|
@ -154,7 +155,7 @@
|
|||
dispatch(
|
||||
'select',
|
||||
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
|
||||
)}>Add</Button
|
||||
)}>{$t('add')}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -168,7 +169,7 @@
|
|||
on:click={() => dispatch('share')}
|
||||
>
|
||||
<Icon path={mdiLink} size={24} />
|
||||
<p class="text-sm">Create link</p>
|
||||
<p class="text-sm">{$t('create_link')}</p>
|
||||
</button>
|
||||
|
||||
{#if sharedLinks.length}
|
||||
|
|
@ -177,7 +178,7 @@
|
|||
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
|
||||
>
|
||||
<Icon path={mdiShareCircle} size={24} />
|
||||
<p class="text-sm">View links</p>
|
||||
<p class="text-sm">{$t('view_links')}</p>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue