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:
Manic-87 2024-06-04 21:53:00 +02:00 committed by GitHub
parent a2bccf23c9
commit f446bc8caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
177 changed files with 2779 additions and 1017 deletions

View file

@ -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}

View file

@ -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>

View file

@ -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}

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -5,6 +5,7 @@
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler } from '$lib/utils/file-uploader';
import { isAlbumsRoute, isSharedLinkRoute } from '$lib/utils/navigation';
import { t } from 'svelte-i18n';
$: albumId = isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined;
$: isShare = isSharedLinkRoute($page.route?.id);
@ -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;
}
};

View file

@ -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}
/>

View file

@ -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}

View file

@ -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>

View file

@ -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"

View file

@ -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}

View file

@ -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

View file

@ -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');

View file

@ -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);

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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}

View file

@ -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>

View file

@ -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">

View file

@ -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">

View file

@ -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}

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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"

View file

@ -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}