mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +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
|
|
@ -7,6 +7,7 @@
|
|||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { initInput } from '$lib/actions/focus';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let albums: AlbumResponseDto[] = [];
|
||||
let recentAlbums: AlbumResponseDto[] = [];
|
||||
|
|
@ -47,9 +48,9 @@
|
|||
|
||||
const getTitle = () => {
|
||||
if (shared) {
|
||||
return 'Add to shared album';
|
||||
return $t('add_to_shared_album');
|
||||
}
|
||||
return 'Add to album';
|
||||
return $t('add_to_album');
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
{:else}
|
||||
<input
|
||||
class="border-b-4 border-immich-bg bg-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:focus:border-immich-dark-primary"
|
||||
placeholder="Search"
|
||||
placeholder={$t('search')}
|
||||
bind:value={search}
|
||||
use:initInput
|
||||
/>
|
||||
|
|
@ -90,7 +91,7 @@
|
|||
</button>
|
||||
{#if filteredAlbums.length > 0}
|
||||
{#if !shared && search.length === 0}
|
||||
<p class="px-5 py-3 text-xs">RECENT</p>
|
||||
<p class="px-5 py-3 text-xs">{$t('recent').toUpperCase()}</p>
|
||||
{#each recentAlbums as album (album.id)}
|
||||
<AlbumListItem {album} on:album={() => handleSelect(album)} />
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import ConfirmDialog from './dialog/confirm-dialog.svelte';
|
||||
import Combobox from './combobox.svelte';
|
||||
import DateInput from '../elements/date-input.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let initialDate: DateTime = DateTime.now();
|
||||
|
||||
|
|
@ -57,7 +58,7 @@
|
|||
|
||||
<ConfirmDialog
|
||||
confirmColor="primary"
|
||||
title="Edit date and time"
|
||||
title={$t('edit_date_and_time')}
|
||||
prompt="Please select a new date:"
|
||||
disabled={!date.isValid}
|
||||
onConfirm={handleConfirm}
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
>
|
||||
<div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt">
|
||||
<div class="flex flex-col">
|
||||
<label for="datetime">Date and Time</label>
|
||||
<label for="datetime">{$t('date_and_time')}</label>
|
||||
<DateInput
|
||||
class="immich-form-input text-sm my-4 w-full"
|
||||
id="datetime"
|
||||
|
|
@ -74,7 +75,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<Combobox bind:selectedOption label="Timezone" options={timezones} placeholder="Search timezone..." />
|
||||
<Combobox bind:selectedOption label={$t('timezone')} options={timezones} placeholder={$t('search_timezone')} />
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
|
||||
import SearchBar from '../elements/search-bar.svelte';
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let asset: AssetResponseDto | undefined = undefined;
|
||||
|
||||
|
|
@ -88,7 +89,7 @@
|
|||
// skip error when a newer search is happening
|
||||
if (latestSearchTimeout === searchTimeout) {
|
||||
places = [];
|
||||
handleError(error, "Can't search places");
|
||||
handleError(error, $t('cant_search_places'));
|
||||
showLoadingSpinner = false;
|
||||
}
|
||||
});
|
||||
|
|
@ -105,7 +106,7 @@
|
|||
|
||||
<ConfirmDialog
|
||||
confirmColor="primary"
|
||||
title="Change location"
|
||||
title={$t('change_location')}
|
||||
width="wide"
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
|
|
@ -118,7 +119,7 @@
|
|||
>
|
||||
<button type="button" class="w-full" on:click={() => (hideSuggestion = false)}>
|
||||
<SearchBar
|
||||
placeholder="Search places"
|
||||
placeholder={$t('search_places')}
|
||||
bind:name={searchWord}
|
||||
{showLoadingSpinner}
|
||||
on:reset={() => {
|
||||
|
|
@ -147,7 +148,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<label for="datetime">Pick a location</label>
|
||||
<label for="datetime">{$t('pick_a_location')}</label>
|
||||
<div class="h-[500px] min-h-[300px] w-full">
|
||||
{#await import('../shared-components/map/map.svelte')}
|
||||
{#await delay(timeToLoadTheMap) then}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let label: string;
|
||||
export let hideLabel = false;
|
||||
|
|
@ -200,7 +201,7 @@
|
|||
class:pointer-events-none={!selectedOption}
|
||||
>
|
||||
{#if selectedOption}
|
||||
<CircleIconButton on:click={onClear} title="Clear value" icon={mdiClose} size="16" padding="2" />
|
||||
<CircleIconButton on:click={onClear} title={$t('clear_value')} icon={mdiClose} size="16" padding="2" />
|
||||
{:else if !isOpen}
|
||||
<Icon path={mdiUnfoldMoreHorizontal} ariaHidden={true} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { fly } from 'svelte/transition';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let showBackButton = true;
|
||||
export let backIcon = mdiClose;
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
>
|
||||
<div class="flex place-items-center gap-6 justify-self-start dark:text-immich-dark-fg">
|
||||
{#if showBackButton}
|
||||
<CircleIconButton title="Close" on:click={handleClose} icon={backIcon} size={'24'} class={buttonClass} />
|
||||
<CircleIconButton title={$t('close')} on:click={handleClose} icon={backIcon} size={'24'} class={buttonClass} />
|
||||
{/if}
|
||||
<slot name="leading" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '../settings/setting-switch.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let onClose: () => void;
|
||||
export let albumId: string | undefined = undefined;
|
||||
|
|
@ -35,8 +36,18 @@
|
|||
}>();
|
||||
|
||||
const expiredDateOption: ImmichDropDownOption = {
|
||||
default: 'Never',
|
||||
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days', '3 months', '1 year'],
|
||||
default: $t('never'),
|
||||
options: [
|
||||
$t('never'),
|
||||
$t('durations.minutes', { values: { minutes: 30 } }),
|
||||
$t('durations.hours', { values: { hours: 1 } }),
|
||||
$t('durations.hours', { values: { hours: 6 } }),
|
||||
$t('durations.days', { values: { days: 1 } }),
|
||||
$t('durations.days', { values: { days: 7 } }),
|
||||
$t('durations.days', { values: { days: 30 } }),
|
||||
$t('durations.months', { values: { months: 3 } }),
|
||||
$t('durations.years', { values: { years: 1 } }),
|
||||
],
|
||||
};
|
||||
|
||||
$: shareType = albumId ? SharedLinkType.Album : SharedLinkType.Individual;
|
||||
|
|
@ -142,7 +153,7 @@
|
|||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Edited',
|
||||
message: $t('edited'),
|
||||
});
|
||||
|
||||
onClose();
|
||||
|
|
@ -153,9 +164,9 @@
|
|||
|
||||
const getTitle = () => {
|
||||
if (editingLink) {
|
||||
return 'Edit link';
|
||||
return $t('edit_link');
|
||||
}
|
||||
return 'Create link to share';
|
||||
return $t('create_link_to_share');
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -186,29 +197,33 @@
|
|||
{/if}
|
||||
|
||||
<div class="mb-2 mt-4">
|
||||
<p class="text-xs">LINK OPTIONS</p>
|
||||
<p class="text-xs">{$t('link_options').toUpperCase()}</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 overflow-y-auto">
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2">
|
||||
<SettingInputField inputType={SettingInputFieldType.TEXT} label="Description" bind:value={description} />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('description')}
|
||||
bind:value={description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Password"
|
||||
label={$t('password')}
|
||||
bind:value={password}
|
||||
disabled={!enablePassword}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={enablePassword} title={'Require password'} />
|
||||
<SettingSwitch bind:checked={enablePassword} title={$t('require_password')} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={showMetadata} title={'Show metadata'} />
|
||||
<SettingSwitch bind:checked={showMetadata} title={$t('show_metadata')} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
|
|
@ -222,10 +237,10 @@
|
|||
<div class="text-sm">
|
||||
{#if editingLink}
|
||||
<p class="immich-form-label my-2">
|
||||
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={'Change expiration time'} />
|
||||
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
|
||||
</p>
|
||||
{:else}
|
||||
<p class="immich-form-label my-2">Expire after</p>
|
||||
<p class="immich-form-label my-2">{$t('expire_after')}</p>
|
||||
{/if}
|
||||
|
||||
<DropdownButton
|
||||
|
|
@ -241,16 +256,16 @@
|
|||
<svelte:fragment slot="sticky-bottom">
|
||||
{#if !sharedLink}
|
||||
{#if editingLink}
|
||||
<Button size="sm" fullwidth on:click={handleEditLink}>Confirm</Button>
|
||||
<Button size="sm" fullwidth on:click={handleEditLink}>{$t('confirm')}</Button>
|
||||
{:else}
|
||||
<Button size="sm" fullwidth on:click={handleCreateSharedLink}>Create link</Button>
|
||||
<Button size="sm" fullwidth on:click={handleCreateSharedLink}>{$t('create_link')}</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex w-full gap-2">
|
||||
<input class="immich-form-input w-full" bind:value={sharedLink} disabled />
|
||||
<LinkButton on:click={() => (sharedLink ? copyToClipboard(sharedLink) : '')}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiContentCopy} ariaLabel="Copy link to clipboard" size="18" />
|
||||
<Icon path={mdiContentCopy} ariaLabel={$t('copy_link_to_clipboard')} size="18" />
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
import FullScreenModal from '../full-screen-modal.svelte';
|
||||
import Button from '../../elements/buttons/button.svelte';
|
||||
import type { Color } from '$lib/components/elements/buttons/button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let title = 'Confirm';
|
||||
export let title = $t('confirm');
|
||||
export let prompt = 'Are you sure you want to do this?';
|
||||
export let confirmText = 'Confirm';
|
||||
export let confirmText = $t('confirm');
|
||||
export let confirmColor: Color = 'red';
|
||||
export let cancelText = 'Cancel';
|
||||
export let cancelText = $t('cancel');
|
||||
export let cancelColor: Color = 'secondary';
|
||||
export let hideCancelButton = false;
|
||||
export let disabled = false;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
let dragStartTarget: EventTarget | null = null;
|
||||
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes($t('files'))) {
|
||||
dragStartTarget = e.target;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { colorTheme } from '$lib/stores/preferences.store';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { HTMLImgAttributes } from 'svelte/elements';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface $$Props extends HTMLImgAttributes {
|
||||
|
|
@ -21,11 +22,11 @@
|
|||
</script>
|
||||
|
||||
{#if today.month === 4 && today.day === 1}
|
||||
<img src="data:image/png;base64, {alternativeLogo}" alt="Immich Logo" class="h-20" {draggable} />
|
||||
<img src="data:image/png;base64, {alternativeLogo}" alt={$t('immich_logo')} class="h-20" {draggable} />
|
||||
{:else}
|
||||
<img
|
||||
src={noText ? logoNoText : $colorTheme.value == Theme.LIGHT ? logoLightUrl : logoDarkUrl}
|
||||
alt="Immich Logo"
|
||||
alt={$t('immich_logo')}
|
||||
{draggable}
|
||||
{...$$restProps}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
ScaleControl,
|
||||
type Map,
|
||||
} from 'svelte-maplibre';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let mapMarkers: MapMarkerResponseDto[];
|
||||
export let showSettingsModal: boolean | undefined = undefined;
|
||||
|
|
@ -187,7 +188,7 @@
|
|||
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}`
|
||||
: 'Map marker with image'}
|
||||
: $t('map_marker_with_image')}
|
||||
/>
|
||||
{/if}
|
||||
{#if $$slots.popup}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
/**
|
||||
* Unique identifier for the header text.
|
||||
|
|
@ -32,5 +33,5 @@
|
|||
</h1>
|
||||
</div>
|
||||
|
||||
<CircleIconButton on:click={onClose} icon={mdiClose} size={'20'} title="Close" />
|
||||
<CircleIconButton on:click={onClose} icon={mdiClose} size={'20'} title={$t('close')} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
import { NotificationType, notificationController } from '../notification/notification';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import AvatarSelector from './avatar-selector.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let isShowSelectAvatar = false;
|
||||
|
||||
|
|
@ -31,11 +32,11 @@
|
|||
isShowSelectAvatar = false;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Saved profile',
|
||||
message: $t('saved_profile'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save profile');
|
||||
handleError(error, $t('errors.unable_to_save_profile'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
<CircleIconButton
|
||||
color="primary"
|
||||
icon={mdiPencil}
|
||||
title="Edit avatar"
|
||||
title={$t('edit_avatar')}
|
||||
class="border"
|
||||
size="12"
|
||||
padding="2"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import FullScreenModal from '../full-screen-modal.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
|
|
@ -13,7 +14,7 @@
|
|||
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
|
||||
</script>
|
||||
|
||||
<FullScreenModal title="Select avatar color" width="auto" onClose={() => dispatch('close')}>
|
||||
<FullScreenModal title={$t('select_avatar_color')} width="auto" onClose={() => dispatch('close')}>
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{#each colors as color}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import UserAvatar from '../user-avatar.svelte';
|
||||
import AccountInfoPanel from './account-info-panel.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let showUploadButton = true;
|
||||
|
||||
|
|
@ -42,7 +43,7 @@
|
|||
<svelte:window bind:innerWidth />
|
||||
|
||||
<section id="dashboard-navbar" class="fixed z-[900] h-[var(--navbar-height)] w-screen text-sm">
|
||||
<SkipLink>Skip to content</SkipLink>
|
||||
<SkipLink>{$t('skip_to_content')}</SkipLink>
|
||||
<div
|
||||
class="grid h-full grid-cols-[theme(spacing.18)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
||||
>
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
<section class="flex place-items-center justify-end gap-4 max-sm:w-full">
|
||||
{#if $featureFlags.search}
|
||||
<a href={AppRoute.SEARCH} id="search-button" class="ml-4 sm:hidden">
|
||||
<CircleIconButton title="Go to search" icon={mdiMagnify} />
|
||||
<CircleIconButton title={$t('go_to_search')} icon={mdiMagnify} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
|
|
@ -70,7 +71,7 @@
|
|||
<LinkButton on:click={() => dispatch('uploadClicked')}>
|
||||
<div class="flex gap-2">
|
||||
<Icon path={mdiTrayArrowUp} size="1.5em" />
|
||||
<span class="hidden md:block">Upload</span>
|
||||
<span class="hidden md:block">{$t('upload')}</span>
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
|
@ -80,7 +81,7 @@
|
|||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
href={AppRoute.ADMIN_USER_MANAGEMENT}
|
||||
aria-label="Administration"
|
||||
aria-label={$t('administration')}
|
||||
aria-current={$page.url.pathname.includes('/admin') ? 'page' : null}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import { cleanup, render, type RenderResult } from '@testing-library/svelte';
|
||||
import { init } from 'svelte-i18n';
|
||||
import { NotificationType } from '../notification';
|
||||
import NotificationCard from '../notification-card.svelte';
|
||||
|
||||
describe('NotificationCard component', () => {
|
||||
let sut: RenderResult<NotificationCard>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await init({ fallbackLocale: 'en-US' });
|
||||
});
|
||||
|
||||
it('disposes timeout if already removed from the DOM', () => {
|
||||
vi.spyOn(window, 'clearTimeout');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import { render, waitFor, type RenderResult } from '@testing-library/svelte';
|
||||
import { init } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { NotificationType, notificationController } from '../notification';
|
||||
import NotificationList from '../notification-list.svelte';
|
||||
|
|
@ -11,7 +12,8 @@ function _getNotificationListElement(sut: RenderResult<NotificationList>): HTMLA
|
|||
describe('NotificationList component', () => {
|
||||
const sut: RenderResult<NotificationList> = render(NotificationList);
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await init({ fallbackLocale: 'en-US' });
|
||||
// https://testing-library.com/docs/svelte-testing-library/faq#why-arent-transition-events-running
|
||||
vi.stubGlobal('requestAnimationFrame', (fn: FrameRequestCallback) => {
|
||||
setTimeout(() => fn(Date.now()), 16);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { mdiCloseCircleOutline, mdiInformationOutline, mdiWindowClose } from '@mdi/js';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let notification: Notification;
|
||||
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
</div>
|
||||
<CircleIconButton
|
||||
icon={mdiWindowClose}
|
||||
title="Close"
|
||||
title={$t('close')}
|
||||
class="dark:text-immich-dark-gray"
|
||||
size="20"
|
||||
padding="2"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface $$Props extends HTMLInputAttributes {
|
||||
password: string;
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
tabindex="-1"
|
||||
class="absolute inset-y-0 end-0 px-4 text-gray-700 dark:text-gray-200"
|
||||
on:click={() => (showPassword = !showPassword)}
|
||||
title={showPassword ? 'Hide password' : 'Show password'}
|
||||
title={showPassword ? $t('hide_password') : $t('show_password')}
|
||||
>
|
||||
<Icon path={showPassword ? mdiEyeOffOutline : mdiEyeOutline} size="1.25em" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import Button from '../elements/buttons/button.svelte';
|
||||
import { NotificationType, notificationController } from './notification/notification';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let onClose: () => void;
|
||||
|
|
@ -58,18 +59,18 @@
|
|||
const { profileImagePath } = await createProfileImage({ createProfileImageDto: { file } });
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Profile picture set.',
|
||||
message: $t('profile_picture_set'),
|
||||
timeout: 3000,
|
||||
});
|
||||
$user.profileImagePath = profileImagePath;
|
||||
} catch (error) {
|
||||
handleError(error, 'Error setting profile picture.');
|
||||
handleError(error, $t('errors.unable_to_set_profile_picture'));
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal title="Set profile picture" width="auto" {onClose}>
|
||||
<FullScreenModal title={$t('set_profile_picture')} width="auto" {onClose}>
|
||||
<div class="flex place-items-center items-center justify-center">
|
||||
<div
|
||||
class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
|
||||
|
|
@ -78,6 +79,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button fullwidth on:click={handleSetProfilePicture}>Set as profile picture</Button>
|
||||
<Button fullwidth on:click={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let value = '';
|
||||
export let grayTheme: boolean;
|
||||
|
|
@ -103,9 +104,9 @@
|
|||
on:submit|preventDefault={onSubmit}
|
||||
>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<CircleIconButton type="submit" title="Search" icon={mdiMagnify} size="20" />
|
||||
<CircleIconButton type="submit" title={$t('search')} icon={mdiMagnify} size="20" />
|
||||
</div>
|
||||
<label for="main-search-bar" class="sr-only">Search your photos</label>
|
||||
<label for="main-search-bar" class="sr-only">{$t('search_your_photos')}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
|
|
@ -117,7 +118,7 @@
|
|||
showFilter
|
||||
? 'rounded-t-3xl border border-gray-200 bg-white dark:border-gray-800'
|
||||
: 'rounded-3xl border border-transparent bg-gray-200'}"
|
||||
placeholder="Search your photos"
|
||||
placeholder={$t('search_your_photos')}
|
||||
required
|
||||
pattern="^(?!m:$).*$"
|
||||
bind:value
|
||||
|
|
@ -132,11 +133,11 @@
|
|||
/>
|
||||
|
||||
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-2'} flex items-center pl-6 transition-all">
|
||||
<CircleIconButton title="Show search options" icon={mdiTune} on:click={onFilterClick} size="20" />
|
||||
<CircleIconButton title={$t('show_search_options')} icon={mdiTune} on:click={onFilterClick} size="20" />
|
||||
</div>
|
||||
{#if showClearIcon}
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<CircleIconButton type="reset" icon={mdiClose} title="Clear" size="20" />
|
||||
<CircleIconButton type="reset" icon={mdiClose} title={$t('clear')} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk';
|
||||
import Combobox, { toComboBoxOptions } from '../combobox.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let filters: SearchCameraFilter;
|
||||
|
||||
|
|
@ -36,25 +37,25 @@
|
|||
</script>
|
||||
|
||||
<div id="camera-selection">
|
||||
<p class="immich-form-label">CAMERA</p>
|
||||
<p class="immich-form-label">{$t('camera').toUpperCase()}</p>
|
||||
|
||||
<div class="grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-5 mt-1">
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label="Make"
|
||||
label={$t('make')}
|
||||
on:select={({ detail }) => (filters.make = detail?.value)}
|
||||
options={toComboBoxOptions(makes)}
|
||||
placeholder="Search camera make..."
|
||||
placeholder={$t('search_camera_make')}
|
||||
selectedOption={makeFilter ? { label: makeFilter, value: makeFilter } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label="Model"
|
||||
label={$t('model')}
|
||||
on:select={({ detail }) => (filters.model = detail?.value)}
|
||||
options={toComboBoxOptions(models)}
|
||||
placeholder="Search camera model..."
|
||||
placeholder={$t('search_camera_model')}
|
||||
selectedOption={modelFilter ? { label: modelFilter, value: modelFilter } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
<script lang="ts">
|
||||
import DateInput from '$lib/components/elements/date-input.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let filters: SearchDateFilter;
|
||||
</script>
|
||||
|
||||
<div id="date-range-selection" class="grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-5">
|
||||
<label class="immich-form-label" for="start-date">
|
||||
<span>START DATE</span>
|
||||
<span>{$t('start_date').toUpperCase()}</span>
|
||||
<DateInput
|
||||
class="immich-form-input w-full mt-1 hover:cursor-pointer"
|
||||
type="date"
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
</label>
|
||||
|
||||
<label class="immich-form-label" for="end-date">
|
||||
<span>END DATE</span>
|
||||
<span>{$t('end_date').toUpperCase()}</span>
|
||||
<DateInput
|
||||
class="immich-form-input w-full mt-1 hover:cursor-pointer"
|
||||
type="date"
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let filters: SearchDisplayFilters;
|
||||
</script>
|
||||
|
||||
<div id="display-options-selection">
|
||||
<fieldset>
|
||||
<legend class="immich-form-label">DISPLAY OPTIONS</legend>
|
||||
<legend class="immich-form-label">{$t('display_options').toUpperCase()}</legend>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
|
||||
<Checkbox id="not-in-album-checkbox" label="Not in any album" bind:checked={filters.isNotInAlbum} />
|
||||
<Checkbox id="archive-checkbox" label="Archive" bind:checked={filters.isArchive} />
|
||||
<Checkbox id="favorite-checkbox" label="Favorite" bind:checked={filters.isFavorite} />
|
||||
<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} />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
import { parseUtcDate } from '$lib/utils/date-time';
|
||||
import SearchDisplaySection from './search-display-section.svelte';
|
||||
import SearchTextSection from './search-text-section.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let searchQuery: MetadataSearchDto | SmartSearchDto;
|
||||
|
||||
|
|
@ -153,8 +154,8 @@
|
|||
id="button-row"
|
||||
class="flex justify-end gap-4 border-t dark:border-gray-800 dark:bg-immich-dark-gray px-4 sm:py-6 py-4 mt-2 rounded-b-3xl"
|
||||
>
|
||||
<Button type="reset" color="gray">Clear all</Button>
|
||||
<Button type="submit">Search</Button>
|
||||
<Button type="reset" color="gray">{$t('clear_all')}</Button>
|
||||
<Button type="submit">{$t('search')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { mdiMagnify, mdiClose } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
selectSearchTerm: string;
|
||||
|
|
@ -18,12 +19,12 @@
|
|||
>
|
||||
{#if $savedSearchTerms.length > 0}
|
||||
<div class="flex items-center justify-between px-5 pt-5 text-xs">
|
||||
<p>RECENT SEARCHES</p>
|
||||
<p>{$t('recent_searches').toUpperCase()}</p>
|
||||
<div class="flex w-18 items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 font-semibold text-immich-primary hover:bg-immich-primary/25 dark:text-immich-dark-primary"
|
||||
on:click={() => dispatch('clearAllSearchTerms')}>Clear all</button
|
||||
on:click={() => dispatch('clearAllSearchTerms')}>{$t('clear_all')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
|
||||
import Combobox, { toComboBoxOptions } from '../combobox.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let filters: SearchLocationFilter;
|
||||
|
||||
|
|
@ -58,35 +59,35 @@
|
|||
</script>
|
||||
|
||||
<div id="location-selection">
|
||||
<p class="immich-form-label">PLACE</p>
|
||||
<p class="immich-form-label">{$t('place').toUpperCase()}</p>
|
||||
|
||||
<div class="grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-5 mt-1">
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label="Country"
|
||||
label={$t('country')}
|
||||
on:select={({ detail }) => (filters.country = detail?.value)}
|
||||
options={toComboBoxOptions(countries)}
|
||||
placeholder="Search country..."
|
||||
placeholder={$t('search_country')}
|
||||
selectedOption={filters.country ? { label: filters.country, value: filters.country } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label="State"
|
||||
label={$t('state')}
|
||||
on:select={({ detail }) => (filters.state = detail?.value)}
|
||||
options={toComboBoxOptions(states)}
|
||||
placeholder="Search state..."
|
||||
placeholder={$t('search_state')}
|
||||
selectedOption={filters.state ? { label: filters.state, value: filters.state } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label="City"
|
||||
label={$t('city')}
|
||||
on:select={({ detail }) => (filters.city = detail?.value)}
|
||||
options={toComboBoxOptions(cities)}
|
||||
placeholder="Search city..."
|
||||
placeholder={$t('search_city')}
|
||||
selectedOption={filters.city ? { label: filters.city, value: filters.city } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
<script lang="ts">
|
||||
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
||||
import { MediaType } from './search-filter-box.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let filteredMedia: MediaType;
|
||||
</script>
|
||||
|
||||
<div id="media-type-selection">
|
||||
<fieldset>
|
||||
<legend class="immich-form-label">MEDIA TYPE</legend>
|
||||
<legend class="immich-form-label">{$t('media_type').toUpperCase()}</legend>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
|
||||
<RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label="All" value={MediaType.All} />
|
||||
<RadioButton name="media-type" id="type-image" bind:group={filteredMedia} label="Image" value={MediaType.Image} />
|
||||
<RadioButton name="media-type" id="type-video" bind:group={filteredMedia} label="Video" value={MediaType.Video} />
|
||||
<RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label={$t('all')} value={MediaType.All} />
|
||||
<RadioButton
|
||||
name="media-type"
|
||||
id="type-image"
|
||||
bind:group={filteredMedia}
|
||||
label={$t('image')}
|
||||
value={MediaType.Image}
|
||||
/>
|
||||
<RadioButton
|
||||
name="media-type"
|
||||
id="type-video"
|
||||
bind:group={filteredMedia}
|
||||
label={$t('video')}
|
||||
value={MediaType.Video}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
||||
import { mdiClose, mdiArrowRight } from '@mdi/js';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let width: number;
|
||||
export let selectedPeople: Set<string>;
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
const res = await getAllPeople({ withHidden: false });
|
||||
return orderBySelectedPeopleFirst(res.people);
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to get people');
|
||||
handleError(error, $t('failed_to_get_people'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,8 +56,8 @@
|
|||
|
||||
<div id="people-selection" class="-mb-4">
|
||||
<div class="flex items-center w-full justify-between gap-6">
|
||||
<p class="immich-form-label py-3">PEOPLE</p>
|
||||
<SearchBar bind:name placeholder="Filter people" showLoadingSpinner={false} />
|
||||
<p class="immich-form-label py-3">{$t('people').toUpperCase()}</p>
|
||||
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
|
||||
</div>
|
||||
|
||||
<div class="flex -mx-1 max-h-64 gap-1 mt-2 flex-wrap overflow-y-auto immich-scrollbar">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let filename: string | undefined;
|
||||
export let context: string | undefined;
|
||||
|
|
@ -21,33 +22,33 @@
|
|||
</script>
|
||||
|
||||
<fieldset>
|
||||
<legend class="immich-form-label">Search type</legend>
|
||||
<legend class="immich-form-label">{$t('search_type')}</legend>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1 mb-2">
|
||||
<RadioButton
|
||||
name="query-type"
|
||||
id="context-radio"
|
||||
bind:group={selectedOption}
|
||||
label="Context"
|
||||
label={$t('context')}
|
||||
value={TextSearchOptions.Context}
|
||||
/>
|
||||
<RadioButton
|
||||
name="query-type"
|
||||
id="file-name-radio"
|
||||
bind:group={selectedOption}
|
||||
label="File name or extension"
|
||||
label={$t('file_name_or_extension')}
|
||||
value={TextSearchOptions.Filename}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{#if selectedOption === TextSearchOptions.Context}
|
||||
<label for="context-input" class="immich-form-label">Search by context</label>
|
||||
<label for="context-input" class="immich-form-label">{$t('search_by_context')}</label>
|
||||
<input
|
||||
class="immich-form-input hover:cursor-text w-full !mt-1"
|
||||
type="text"
|
||||
id="context-input"
|
||||
name="context"
|
||||
placeholder="Sunrise on the beach"
|
||||
placeholder={$t('sunrise_on_the_beach')}
|
||||
bind:value={context}
|
||||
/>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
reset: ResetOptions;
|
||||
|
|
@ -26,7 +27,9 @@
|
|||
</div>
|
||||
|
||||
<div class="right">
|
||||
<Button {disabled} size="sm" color="gray" on:click={() => dispatch('reset', { default: false })}>Reset</Button>
|
||||
<Button type="submit" {disabled} size="sm" on:click={() => dispatch('save')}>Save</Button>
|
||||
<Button {disabled} size="sm" color="gray" on:click={() => dispatch('reset', { default: false })}
|
||||
>{$t('reset')}</Button
|
||||
>
|
||||
<Button type="submit" {disabled} size="sm" on:click={() => dispatch('save')}>{$t('save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
import { mdiInformationOutline } from '@mdi/js';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Shortcuts {
|
||||
general: ExplainedShortcut[];
|
||||
|
|
@ -17,18 +18,18 @@
|
|||
|
||||
const shortcuts: Shortcuts = {
|
||||
general: [
|
||||
{ key: ['←', '→'], action: 'Previous or next photo' },
|
||||
{ key: ['←', '→'], action: $t('previous_or_next_photo') },
|
||||
{ key: ['Esc'], action: 'Back, close, or deselect' },
|
||||
{ key: ['Ctrl', 'k'], action: 'Search your photos' },
|
||||
{ key: ['Ctrl', '⇧', 'k'], action: 'Open the search filters' },
|
||||
{ key: ['Ctrl', 'k'], action: $t('search_your_photos') },
|
||||
{ key: ['Ctrl', '⇧', 'k'], action: $t('open_the_search_filters') },
|
||||
],
|
||||
actions: [
|
||||
{ key: ['f'], action: 'Favorite or unfavorite photo' },
|
||||
{ key: ['i'], action: 'Show or hide info' },
|
||||
{ key: ['s'], action: 'Stack selected photos' },
|
||||
{ key: ['⇧', 'a'], action: 'Archive or unarchive photo' },
|
||||
{ key: ['⇧', 'd'], action: 'Download' },
|
||||
{ key: ['Space'], action: 'Play or pause video' },
|
||||
{ key: ['f'], action: $t('favorite_or_unfavorite_photo') },
|
||||
{ key: ['i'], action: $t('show_or_hide_info') },
|
||||
{ key: ['s'], action: $t('stack_selected_photos') },
|
||||
{ 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' },
|
||||
],
|
||||
};
|
||||
|
|
@ -37,10 +38,10 @@
|
|||
}>();
|
||||
</script>
|
||||
|
||||
<FullScreenModal title="Keyboard shortcuts" width="auto" onClose={() => dispatch('close')}>
|
||||
<FullScreenModal title={$t('keyboard_shortcuts')} width="auto" onClose={() => dispatch('close')}>
|
||||
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
|
||||
<div class="p-4">
|
||||
<h2>General</h2>
|
||||
<h2>{$t('general')}</h2>
|
||||
<div class="text-sm">
|
||||
{#each shortcuts.general as shortcut}
|
||||
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h2>Actions</h2>
|
||||
<h2>{$t('actions')}</h2>
|
||||
<div class="text-sm">
|
||||
{#each shortcuts.actions as shortcut}
|
||||
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@
|
|||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SideBarSection>
|
||||
<nav aria-label="Primary">
|
||||
<SideBarLink title="Users" routeId={AppRoute.ADMIN_USER_MANAGEMENT} icon={mdiAccountMultipleOutline} />
|
||||
<SideBarLink title="Jobs" routeId={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||
<SideBarLink title="Settings" routeId={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<SideBarLink title="External Libraries" routeId={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||
<SideBarLink title="Server Stats" routeId={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||
<SideBarLink title="Repair" routeId={AppRoute.ADMIN_REPAIR} icon={mdiTools} preloadData={false} />
|
||||
<nav aria-label={$t('primary')}>
|
||||
<SideBarLink title={$t('users')} routeId={AppRoute.ADMIN_USER_MANAGEMENT} icon={mdiAccountMultipleOutline} />
|
||||
<SideBarLink title={$t('jobs')} routeId={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||
<SideBarLink title={$t('settings')} routeId={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<SideBarLink title={$t('external_libraries')} routeId={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||
<SideBarLink title={$t('server_stats')} routeId={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||
<SideBarLink title={$t('repair')} routeId={AppRoute.ADMIN_REPAIR} icon={mdiTools} preloadData={false} />
|
||||
</nav>
|
||||
|
||||
<div class="mb-6 mt-auto">
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
import SideBarLink from './side-bar-link.svelte';
|
||||
import MoreInformationAssets from '$lib/components/shared-components/side-bar/more-information-assets.svelte';
|
||||
import MoreInformationAlbums from '$lib/components/shared-components/side-bar/more-information-albums.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let isArchiveSelected: boolean;
|
||||
let isFavoritesSelected: boolean;
|
||||
|
|
@ -38,9 +39,9 @@
|
|||
</script>
|
||||
|
||||
<SideBarSection>
|
||||
<nav aria-label="Primary">
|
||||
<nav aria-label={$t('primary')}>
|
||||
<SideBarLink
|
||||
title="Photos"
|
||||
title={$t('photos')}
|
||||
routeId="/(user)/photos"
|
||||
bind:isSelected={isPhotosSelected}
|
||||
icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
|
||||
|
|
@ -50,12 +51,12 @@
|
|||
</svelte:fragment>
|
||||
</SideBarLink>
|
||||
{#if $featureFlags.search}
|
||||
<SideBarLink title="Explore" routeId="/(user)/explore" icon={mdiMagnify} />
|
||||
<SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
|
||||
{/if}
|
||||
|
||||
{#if $featureFlags.map}
|
||||
<SideBarLink
|
||||
title="Map"
|
||||
title={$t('map')}
|
||||
routeId="/(user)/map"
|
||||
bind:isSelected={isMapSelected}
|
||||
icon={isMapSelected ? mdiMap : mdiMapOutline}
|
||||
|
|
@ -64,7 +65,7 @@
|
|||
|
||||
{#if $sidebarSettings.people}
|
||||
<SideBarLink
|
||||
title="People"
|
||||
title={$t('people')}
|
||||
routeId="/(user)/people"
|
||||
bind:isSelected={isPeopleSelected}
|
||||
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
|
||||
|
|
@ -72,7 +73,7 @@
|
|||
{/if}
|
||||
{#if $sidebarSettings.sharing}
|
||||
<SideBarLink
|
||||
title="Sharing"
|
||||
title={$t('sharing')}
|
||||
routeId="/(user)/sharing"
|
||||
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
|
||||
bind:isSelected={isSharingSelected}
|
||||
|
|
@ -84,11 +85,11 @@
|
|||
{/if}
|
||||
|
||||
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
|
||||
<p class="hidden p-6 group-hover:sm:block md:block">LIBRARY</p>
|
||||
<p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
|
||||
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
|
||||
</div>
|
||||
<SideBarLink
|
||||
title="Favorites"
|
||||
title={$t('favorites')}
|
||||
routeId="/(user)/favorites"
|
||||
icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
|
||||
bind:isSelected={isFavoritesSelected}
|
||||
|
|
@ -97,21 +98,21 @@
|
|||
<MoreInformationAssets assetStats={{ isFavorite: true }} />
|
||||
</svelte:fragment>
|
||||
</SideBarLink>
|
||||
<SideBarLink title="Albums" routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo>
|
||||
<SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo>
|
||||
<svelte:fragment slot="moreInformation">
|
||||
<MoreInformationAlbums albumCountType="owned" />
|
||||
</svelte:fragment>
|
||||
</SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title="Utilities"
|
||||
title={$t('utilities')}
|
||||
routeId="/(user)/utilities"
|
||||
bind:isSelected={isUtilitiesSelected}
|
||||
icon={isUtilitiesSelected ? mdiToolbox : mdiToolboxOutline}
|
||||
></SideBarLink>
|
||||
|
||||
<SideBarLink
|
||||
title="Archive"
|
||||
title={$t('archive')}
|
||||
routeId="/(user)/archive"
|
||||
bind:isSelected={isArchiveSelected}
|
||||
icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
|
||||
|
|
@ -123,7 +124,7 @@
|
|||
|
||||
{#if $featureFlags.trash}
|
||||
<SideBarLink
|
||||
title="Trash"
|
||||
title={$t('trash')}
|
||||
routeId="/(user)/trash"
|
||||
bind:isSelected={isTrashSelected}
|
||||
icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const { serverVersion, connected } = websocketStore;
|
||||
|
||||
|
|
@ -52,7 +53,7 @@
|
|||
<Icon path={mdiChartPie} size="24" />
|
||||
</div>
|
||||
<div class="hidden group-hover:sm:block md:block">
|
||||
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Storage</p>
|
||||
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">{$t('storage')}</p>
|
||||
{#if $serverInfo}
|
||||
<div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
|
||||
|
|
@ -76,20 +77,20 @@
|
|||
<Icon path={mdiDns} size="26" />
|
||||
</div>
|
||||
<div class="hidden text-xs group-hover:sm:block md:block">
|
||||
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">Server</p>
|
||||
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">{$t('server')}</p>
|
||||
|
||||
<div class="mt-2 flex justify-between justify-items-center">
|
||||
<p>Status</p>
|
||||
<p>{$t('status')}</p>
|
||||
|
||||
{#if $connected}
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">Online</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('online')}</p>
|
||||
{:else}
|
||||
<p class="font-medium text-red-500">Offline</p>
|
||||
<p class="font-medium text-red-500">{$t('offline')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-between justify-items-center">
|
||||
<p>Version</p>
|
||||
<p>{$t('version')}</p>
|
||||
{#if $connected && version}
|
||||
<a
|
||||
href="https://github.com/immich-app/immich/releases"
|
||||
|
|
@ -99,7 +100,7 @@
|
|||
{version}
|
||||
</a>
|
||||
{:else}
|
||||
<p class="font-medium text-red-500">Unknown</p>
|
||||
<p class="font-medium text-red-500">{$t('unknown')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { colorTheme, handleToggleTheme } from '$lib/stores/preferences.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
$: icon = $colorTheme.value === Theme.LIGHT ? moonPath : sunPath;
|
||||
$: viewBox = $colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox;
|
||||
</script>
|
||||
|
||||
{#if !$colorTheme.system}
|
||||
<CircleIconButton title="Toggle theme" {icon} {viewBox} on:click={handleToggleTheme} />
|
||||
<CircleIconButton title={$t('toggle_theme')} {icon} {viewBox} on:click={handleToggleTheme} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
import { mdiRefresh, mdiCancel } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let uploadAsset: UploadAsset;
|
||||
|
||||
|
|
@ -59,10 +60,10 @@
|
|||
</p>
|
||||
{:else if uploadAsset.state === UploadState.PENDING}
|
||||
<div class="h-[15px] rounded-md bg-immich-dark-gray transition-all dark:bg-immich-gray" style="width: 100%" />
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">Pending</p>
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">{$t('pending')}</p>
|
||||
{:else if uploadAsset.state === UploadState.ERROR}
|
||||
<div class="h-[15px] rounded-md bg-immich-error transition-all" style="width: 100%" />
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">Error</p>
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">{$t('error')}</p>
|
||||
{:else if uploadAsset.state === UploadState.DUPLICATED}
|
||||
<div class="h-[15px] rounded-md bg-immich-warning transition-all" style="width: 100%" />
|
||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
||||
|
|
@ -84,13 +85,13 @@
|
|||
</div>
|
||||
{#if uploadAsset.state === UploadState.ERROR}
|
||||
<div class="flex h-full flex-col place-content-evenly place-items-center justify-items-center pr-2">
|
||||
<button type="button" on:click={() => handleRetry(uploadAsset)} title="Retry upload" class="flex text-sm">
|
||||
<button type="button" on:click={() => handleRetry(uploadAsset)} title={$t('retry_upload')} class="flex text-sm">
|
||||
<span class="text-immich-dark-gray dark:text-immich-dark-fg"><Icon path={mdiRefresh} size="20" /></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => uploadAssetsStore.removeUploadAsset(uploadAsset.id)}
|
||||
title="Dismiss error"
|
||||
title={$t('dismiss_error')}
|
||||
class="flex text-sm"
|
||||
>
|
||||
<span class="text-immich-error"><Icon path={mdiCancel} size="20" /></span>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
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;
|
||||
let showOptions = false;
|
||||
|
|
@ -75,14 +76,14 @@
|
|||
<div class="flex flex-col items-end">
|
||||
<div class="flex flex-row">
|
||||
<CircleIconButton
|
||||
title="Toggle settings"
|
||||
title={$t('toggle_settings')}
|
||||
icon={mdiCog}
|
||||
size="14"
|
||||
padding="1"
|
||||
on:click={() => (showOptions = !showOptions)}
|
||||
/>
|
||||
<CircleIconButton
|
||||
title="Minimize"
|
||||
title={$t('minimize')}
|
||||
icon={mdiWindowMinimize}
|
||||
size="14"
|
||||
padding="1"
|
||||
|
|
@ -91,7 +92,7 @@
|
|||
</div>
|
||||
{#if $hasError}
|
||||
<CircleIconButton
|
||||
title="Dismiss all errors"
|
||||
title={$t('dismiss_all_errors')}
|
||||
icon={mdiCancel}
|
||||
size="14"
|
||||
padding="1"
|
||||
|
|
@ -103,13 +104,13 @@
|
|||
{#if showOptions}
|
||||
<div class="immich-scrollbar mb-4 max-h-[400px] overflow-y-auto rounded-lg pr-2">
|
||||
<div class="flex h-[26px] place-items-center gap-1">
|
||||
<label class="immich-form-label" for="upload-concurrency">Upload concurrency</label>
|
||||
<label class="immich-form-label" for="upload-concurrency">{$t('upload_concurrency')}</label>
|
||||
</div>
|
||||
<input
|
||||
class="immich-form-input w-full"
|
||||
aria-labelledby="Upload concurrency"
|
||||
aria-labelledby={$t('upload_concurrency')}
|
||||
id="upload-concurrency"
|
||||
name="Upload concurrency"
|
||||
name={$t('upload_concurrency')}
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import type { ServerVersionResponseDto } from '@immich/sdk';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let showModal = false;
|
||||
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
</div>
|
||||
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
|
||||
<Button fullwidth on:click={onAcknowledge}>{$t('acknowledge')}</Button>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue