1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(web): add more translations (#10700)

* feat(web): add more translations

* formatting
This commit is contained in:
Michel Heusschen 2024-07-01 00:29:10 +02:00 committed by GitHub
parent e54c18367b
commit c58148af35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 90 additions and 53 deletions

View file

@ -74,7 +74,7 @@
<div class="flex justify-center m-4 gap-2"> <div class="flex justify-center m-4 gap-2">
<Checkbox <Checkbox
id="queue-user-deletion-checkbox" id="queue-user-deletion-checkbox"
label="Queue user and assets for immediate deletion" label={$t('admin.user_delete_immediately_checkbox')}
labelClass="text-sm dark:text-immich-dark-fg" labelClass="text-sm dark:text-immich-dark-fg"
bind:checked={forceDelete} bind:checked={forceDelete}
on:change={() => { on:change={() => {

View file

@ -51,7 +51,7 @@
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p> <p>{$t('admin.authentication_settings_disable_all')}</p>
<p> <p>
<FormatMessage key="admin.authentication_settings_reenable" let:message> <FormatMessage key="admin.authentication_settings_reenable" let:message>
<a <a

View file

@ -18,7 +18,7 @@
export let config: SystemConfigDto; // this is the config that is being edited export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false; export let disabled = false;
const cronExpressionOptions = [ $: cronExpressionOptions = [
{ title: $t('interval.night_at_midnight'), expression: '0 0 * * *' }, { title: $t('interval.night_at_midnight'), expression: '0 0 * * *' },
{ title: $t('interval.night_at_twoam'), expression: '0 2 * * *' }, { title: $t('interval.night_at_twoam'), expression: '0 2 * * *' },
{ title: $t('interval.day_at_onepm'), expression: '0 13 * * *' }, { title: $t('interval.day_at_onepm'), expression: '0 13 * * *' },

View file

@ -11,6 +11,7 @@
SettingInputFieldType, SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte'; } from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let savedConfig: SystemConfigDto; export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto; export let defaultConfig: SystemConfigDto;
@ -52,12 +53,16 @@
<SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}> <SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}>
<svelte:fragment slot="subtitle"> <svelte:fragment slot="subtitle">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
Manage <a <FormatMessage key="admin.map_manage_reverse_geocoding_settings" let:message>
href="https://immich.app/docs/features/reverse-geocoding" <a
class="underline" href="https://immich.app/docs/features/reverse-geocoding"
target="_blank" class="underline"
rel="noreferrer">{$t('admin.map_reverse_geocoding')}</a target="_blank"
> settings rel="noreferrer"
>
{message}
</a>
</FormatMessage>
</p> </p>
</svelte:fragment> </svelte:fragment>
<div class="ml-4 mt-4 flex flex-col gap-4"> <div class="ml-4 mt-4 flex flex-col gap-4">

View file

@ -12,13 +12,13 @@
</script> </script>
<div class="mt-2 text-sm"> <div class="mt-2 text-sm">
<h4>DATE & TIME</h4> <h4>{$t('date_and_time').toUpperCase()}</h4>
</div> </div>
<div class="mt-2 rounded-lg bg-gray-200 p-4 text-xs dark:bg-gray-700 dark:text-immich-dark-fg"> <div class="mt-2 rounded-lg bg-gray-200 p-4 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
<div class="mb-2 text-gray-600 dark:text-immich-dark-fg"> <div class="mb-2 text-gray-600 dark:text-immich-dark-fg">
<p>Asset's creation timestamp is used for the datetime information</p> <p>{$t('admin.storage_template_date_time_description')}</p>
<p>Sample time 2022-02-03T04:56:05.250</p> <p>{$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T04:56:05.250' } })}</p>
</div> </div>
<div class="flex gap-[40px]"> <div class="flex gap-[40px]">
<div> <div>

View file

@ -18,7 +18,7 @@
await updateAsset({ id: asset.id, updateAssetDto: { description: newDescription } }); await updateAsset({ id: asset.id, updateAssetDto: { description: newDescription } });
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: 'Asset description has been updated', message: $t('asset_description_updated'),
}); });
} catch (error) { } catch (error) {
handleError(error, $t('cannot_update_the_description')); handleError(error, $t('cannot_update_the_description'));

View file

@ -4,6 +4,7 @@
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core'; import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { t } from 'svelte-i18n';
export let asset: Pick<AssetResponseDto, 'id' | 'type'>; export let asset: Pick<AssetResponseDto, 'id' | 'type'>;
const photoSphereConfigs = const photoSphereConfigs =
@ -35,6 +36,6 @@
{:then [data, module, adapter, plugins, navbar]} {:then [data, module, adapter, plugins, navbar]}
<svelte:component this={module.default} panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} /> <svelte:component this={module.default} panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} />
{:catch} {:catch}
Failed to load asset {$t('errors.failed_to_load_asset')}
{/await} {/await}
</div> </div>

View file

@ -209,7 +209,7 @@
</div> </div>
{:else} {:else}
{#each peopleWithFaces as face, index} {#each peopleWithFaces as face, index}
{@const personName = face.person ? face.person?.name : 'Unassigned'} {@const personName = face.person ? face.person?.name : $t('face_unassigned')}
<div class="relative z-[20001] h-[115px] w-[95px]"> <div class="relative z-[20001] h-[115px] w-[95px]">
<div <div
role="button" role="button"
@ -261,8 +261,8 @@
curve curve
shadow shadow
url="/src/lib/assets/no-thumbnail.png" url="/src/lib/assets/no-thumbnail.png"
altText="Unassigned" altText={$t('face_unassigned')}
title="Unassigned" title={$t('face_unassigned')}
widthStyle="90px" widthStyle="90px"
heightStyle="90px" heightStyle="90px"
thumbhash={null} thumbhash={null}
@ -273,8 +273,8 @@
curve curve
shadow shadow
url={data === null ? '/src/lib/assets/no-thumbnail.png' : data} url={data === null ? '/src/lib/assets/no-thumbnail.png' : data}
altText="Unassigned" altText={$t('face_unassigned')}
title="Unassigned" title={$t('face_unassigned')}
widthStyle="90px" widthStyle="90px"
heightStyle="90px" heightStyle="90px"
thumbhash={null} thumbhash={null}
@ -289,7 +289,7 @@
{#if selectedPersonToReassign[face.id]?.id} {#if selectedPersonToReassign[face.id]?.id}
{selectedPersonToReassign[face.id]?.name} {selectedPersonToReassign[face.id]?.name}
{:else} {:else}
<span class={personName == 'Unassigned' ? 'dark:text-gray-500' : ''}>{personName}</span> <span class={personName === $t('face_unassigned') ? 'dark:text-gray-500' : ''}>{personName}</span>
{/if} {/if}
</p> </p>
{/if} {/if}
@ -322,7 +322,7 @@
<div <div
class="flex place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform" class="flex place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
> >
<Icon color="primary" path={mdiAccountOff} ariaLabel="Just a face" size="18" /> <Icon color="primary" path={mdiAccountOff} ariaHidden size="18" />
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -57,7 +57,12 @@
<div class="dark:text-immich-dark-fg"> <div class="dark:text-immich-dark-fg">
<div <div
class="storage-status grid grid-cols-[64px_auto]" class="storage-status grid grid-cols-[64px_auto]"
title="Used {getByteUnitString(usedBytes, $locale, 3)} of {getByteUnitString(availableBytes, $locale, 3)}" title={$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale, 3),
available: getByteUnitString(availableBytes, $locale, 3),
},
})}
> >
<div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0"> <div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
<Icon path={mdiChartPie} size="24" /> <Icon path={mdiChartPie} size="24" />

View file

@ -67,7 +67,7 @@
{:else if uploadAsset.state === UploadState.DUPLICATED} {:else if uploadAsset.state === UploadState.DUPLICATED}
<div class="h-[15px] rounded-md bg-immich-warning transition-all" style="width: 100%" /> <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]"> <p class="absolute top-0 h-full w-full text-center text-[10px]">
Skipped {$t('asset_skipped')}
{#if uploadAsset.message} {#if uploadAsset.message}
({uploadAsset.message}) ({uploadAsset.message})
{/if} {/if}
@ -75,7 +75,7 @@
{:else if uploadAsset.state === UploadState.DONE} {:else if uploadAsset.state === UploadState.DONE}
<div class="h-[15px] rounded-md bg-immich-success transition-all" style="width: 100%" /> <div class="h-[15px] rounded-md bg-immich-success transition-all" style="width: 100%" />
<p class="absolute top-0 h-full w-full text-center text-[10px]"> <p class="absolute top-0 h-full w-full text-center text-[10px]">
Uploaded {$t('asset_uploaded')}
{#if uploadAsset.message} {#if uploadAsset.message}
({uploadAsset.message}) ({uploadAsset.message})
{/if} {/if}

View file

@ -5,6 +5,7 @@
<script lang="ts"> <script lang="ts">
import { getProfileImageUrl } from '$lib/utils'; import { getProfileImageUrl } from '$lib/utils';
import { type UserAvatarColor } from '@immich/sdk'; import { type UserAvatarColor } from '@immich/sdk';
import { t } from 'svelte-i18n';
interface User { interface User {
id: string; id: string;
@ -77,7 +78,7 @@
<img <img
bind:this={img} bind:this={img}
src={getProfileImageUrl(user.id)} src={getProfileImageUrl(user.id)}
alt="Profile image of {title}" alt={$t('profile_image_of_user', { values: { user: title } })}
class="h-full w-full object-cover" class="h-full w-full object-cover"
class:hidden={showFallback} class:hidden={showFallback}
draggable="false" draggable="false"

View file

@ -50,7 +50,7 @@
</FormatMessage> </FormatMessage>
</div> </div>
<div class="mt-4 font-medium">Your friend, Alex</div> <div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
<div class="font-sm mt-8"> <div class="font-sm mt-8">
<code>{$t('server_version')}: {serverVersion}</code> <code>{$t('server_version')}: {serverVersion}</code>

View file

@ -51,7 +51,7 @@
<div class="flex flex-col justify-center gap-1 dark:text-white"> <div class="flex flex-col justify-center gap-1 dark:text-white">
<span class="text-sm"> <span class="text-sm">
{#if device.deviceType || device.deviceOS} {#if device.deviceType || device.deviceOS}
<span>{device.deviceOS || 'Unknown'}{device.deviceType || 'Unknown'}</span> <span>{device.deviceOS || $t('unknown')}{device.deviceType || $t('unknown')}</span>
{:else} {:else}
<span>{$t('unknown')}</span> <span>{$t('unknown')}</span>
{/if} {/if}

View file

@ -30,6 +30,7 @@
"add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".", "add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".",
"authentication_settings": "Authentication Settings", "authentication_settings": "Authentication Settings",
"authentication_settings_description": "Manage password, OAuth, and other authentication settings", "authentication_settings_description": "Manage password, OAuth, and other authentication settings",
"authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.",
"authentication_settings_reenable": "To re-enable, use a <link>Server Command</link>.", "authentication_settings_reenable": "To re-enable, use a <link>Server Command</link>.",
"background_task_job": "Background Tasks", "background_task_job": "Background Tasks",
"check_all": "Check All", "check_all": "Check All",
@ -125,6 +126,7 @@
"map_dark_style": "Dark style", "map_dark_style": "Dark style",
"map_enable_description": "Enable map features", "map_enable_description": "Enable map features",
"map_light_style": "Light style", "map_light_style": "Light style",
"map_manage_reverse_geocoding_settings": "Manage <link>Reverse Geocoding</link> settings",
"map_reverse_geocoding": "Reverse Geocoding", "map_reverse_geocoding": "Reverse Geocoding",
"map_reverse_geocoding_enable_description": "Enable reverse geocoding", "map_reverse_geocoding_enable_description": "Enable reverse geocoding",
"map_reverse_geocoding_settings": "Reverse Geocoding Settings", "map_reverse_geocoding_settings": "Reverse Geocoding Settings",
@ -209,6 +211,8 @@
"sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem", "sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem",
"slideshow_duration_description": "Number of seconds to display each image", "slideshow_duration_description": "Number of seconds to display each image",
"smart_search_job_description": "Run machine learning on assets to support smart search", "smart_search_job_description": "Run machine learning on assets to support smart search",
"storage_template_date_time_description": "Asset's creation timestamp is used for the datetime information",
"storage_template_date_time_sample": "Sample time {date}",
"storage_template_enable_description": "Enable storage template engine", "storage_template_enable_description": "Enable storage template engine",
"storage_template_hash_verification_enabled": "Hash verification enabled", "storage_template_hash_verification_enabled": "Hash verification enabled",
"storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications", "storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications",
@ -298,10 +302,12 @@
"user_delete_delay_settings": "Delete delay", "user_delete_delay_settings": "Delete delay",
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.", "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
"user_delete_immediately": "<b>{user}</b>'s account and assets will be queued for permanent deletion <b>immediately</b>.", "user_delete_immediately": "<b>{user}</b>'s account and assets will be queued for permanent deletion <b>immediately</b>.",
"user_delete_immediately_checkbox": "Queue user and assets for immediate deletion",
"user_management": "User Management", "user_management": "User Management",
"user_password_has_been_reset": "The user's password has been reset:", "user_password_has_been_reset": "The user's password has been reset:",
"user_password_reset_description": "Please provide the temporary password to the user and inform them they will need to change the password at their next login.", "user_password_reset_description": "Please provide the temporary password to the user and inform them they will need to change the password at their next login.",
"user_restore_description": "<b>{user}</b>'s account will be restored.", "user_restore_description": "<b>{user}</b>'s account will be restored.",
"user_restore_scheduled_removal": "Restore user - scheduled removal on {date, date, long}",
"user_settings": "User Settings", "user_settings": "User Settings",
"user_settings_description": "Manage user settings", "user_settings_description": "Manage user settings",
"user_successfully_removed": "User {email} has been successfully removed.", "user_successfully_removed": "User {email} has been successfully removed.",
@ -358,10 +364,17 @@
"archived_count": "{count, plural, other {Archived #}}", "archived_count": "{count, plural, other {Archived #}}",
"are_these_the_same_person": "Are these the same person?", "are_these_the_same_person": "Are these the same person?",
"are_you_sure_to_do_this": "Are you sure you want to do this?", "are_you_sure_to_do_this": "Are you sure you want to do this?",
"asset_added_to_album": "Added to album",
"asset_adding_to_album": "Adding to album...",
"asset_description_updated": "Asset description has been updated",
"asset_filename_is_offline": "Asset {filename} is offline", "asset_filename_is_offline": "Asset {filename} is offline",
"asset_has_unassigned_faces": "Asset has unassigned faces", "asset_has_unassigned_faces": "Asset has unassigned faces",
"asset_hashing": "Hashing...",
"asset_offline": "Asset offline", "asset_offline": "Asset offline",
"asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.", "asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.",
"asset_skipped": "Skipped",
"asset_uploaded": "Uploaded",
"asset_uploading": "Uploading...",
"assets": "Assets", "assets": "Assets",
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}", "assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album", "assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
@ -456,6 +469,7 @@
"date_after": "Date after", "date_after": "Date after",
"date_and_time": "Date and Time", "date_and_time": "Date and Time",
"date_before": "Date before", "date_before": "Date before",
"date_of_birth_saved": "Date of birth saved successfully",
"date_range": "Date range", "date_range": "Date range",
"day": "Day", "day": "Day",
"deduplicate_all": "Deduplicate All", "deduplicate_all": "Deduplicate All",
@ -544,6 +558,8 @@
"failed_to_create_shared_link": "Failed to create shared link", "failed_to_create_shared_link": "Failed to create shared link",
"failed_to_edit_shared_link": "Failed to edit shared link", "failed_to_edit_shared_link": "Failed to edit shared link",
"failed_to_get_people": "Failed to get people", "failed_to_get_people": "Failed to get people",
"failed_to_load_asset": "Failed to load asset",
"failed_to_load_assets": "Failed to load assets",
"failed_to_stack_assets": "Failed to stack assets", "failed_to_stack_assets": "Failed to stack assets",
"failed_to_unstack_assets": "Failed to un-stack assets", "failed_to_unstack_assets": "Failed to un-stack assets",
"import_path_already_exists": "This import path already exists.", "import_path_already_exists": "This import path already exists.",
@ -569,6 +585,7 @@
"unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}", "unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}",
"unable_to_complete_oauth_login": "Unable to complete OAuth login", "unable_to_complete_oauth_login": "Unable to complete OAuth login",
"unable_to_connect": "Unable to connect", "unable_to_connect": "Unable to connect",
"unable_to_connect_to_server": "Unable to connect to server",
"unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https",
"unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_admin_account": "Unable to create admin account",
"unable_to_create_api_key": "Unable to create a new API Key", "unable_to_create_api_key": "Unable to create a new API Key",
@ -588,6 +605,7 @@
"unable_to_enter_fullscreen": "Unable to enter fullscreen", "unable_to_enter_fullscreen": "Unable to enter fullscreen",
"unable_to_exit_fullscreen": "Unable to exit fullscreen", "unable_to_exit_fullscreen": "Unable to exit fullscreen",
"unable_to_get_comments_number": "Unable to get number of comments", "unable_to_get_comments_number": "Unable to get number of comments",
"unable_to_get_shared_link": "Failed to get shared link",
"unable_to_hide_person": "Unable to hide person", "unable_to_hide_person": "Unable to hide person",
"unable_to_link_oauth_account": "Unable to link OAuth account", "unable_to_link_oauth_account": "Unable to link OAuth account",
"unable_to_load_album": "Unable to load album", "unable_to_load_album": "Unable to load album",
@ -616,6 +634,7 @@
"unable_to_restore_user": "Unable to restore user", "unable_to_restore_user": "Unable to restore user",
"unable_to_save_album": "Unable to save album", "unable_to_save_album": "Unable to save album",
"unable_to_save_api_key": "Unable to save API Key", "unable_to_save_api_key": "Unable to save API Key",
"unable_to_save_date_of_birth": "Unable to save date of birth",
"unable_to_save_name": "Unable to save name", "unable_to_save_name": "Unable to save name",
"unable_to_save_profile": "Unable to save profile", "unable_to_save_profile": "Unable to save profile",
"unable_to_save_settings": "Unable to save settings", "unable_to_save_settings": "Unable to save settings",
@ -632,7 +651,8 @@
"unable_to_update_location": "Unable to update location", "unable_to_update_location": "Unable to update location",
"unable_to_update_settings": "Unable to update settings", "unable_to_update_settings": "Unable to update settings",
"unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_timeline_display_status": "Unable to update timeline display status",
"unable_to_update_user": "Unable to update user" "unable_to_update_user": "Unable to update user",
"unable_to_upload_file": "Unable to upload file"
}, },
"exif": "Exif", "exif": "Exif",
"exit_slideshow": "Exit Slideshow", "exit_slideshow": "Exit Slideshow",
@ -646,6 +666,7 @@
"extension": "Extension", "extension": "Extension",
"external": "External", "external": "External",
"external_libraries": "External Libraries", "external_libraries": "External Libraries",
"face_unassigned": "Unassigned",
"favorite": "Favorite", "favorite": "Favorite",
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites", "favorites": "Favorites",
@ -870,6 +891,7 @@
"previous_memory": "Previous memory", "previous_memory": "Previous memory",
"previous_or_next_photo": "Previous or next photo", "previous_or_next_photo": "Previous or next photo",
"primary": "Primary", "primary": "Primary",
"profile_image_of_user": "Profile image of {title}",
"profile_picture_set": "Profile picture set.", "profile_picture_set": "Profile picture set.",
"public_album": "Public album", "public_album": "Public album",
"public_share": "Public Share", "public_share": "Public Share",
@ -991,6 +1013,7 @@
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}", "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
"shared_with_partner": "Shared with {partner}", "shared_with_partner": "Shared with {partner}",
"sharing": "Sharing", "sharing": "Sharing",
"sharing_enter_password": "Please enter the password to view this page.",
"sharing_sidebar_description": "Display a link to Sharing in the sidebar", "sharing_sidebar_description": "Display a link to Sharing in the sidebar",
"shift_to_permanent_delete": "press ⇧ to permanently delete asset", "shift_to_permanent_delete": "press ⇧ to permanently delete asset",
"show_album_options": "Show album options", "show_album_options": "Show album options",
@ -1107,6 +1130,7 @@
"validate": "Validate", "validate": "Validate",
"variables": "Variables", "variables": "Variables",
"version": "Version", "version": "Version",
"version_announcement_closing": "Your friend, Alex",
"version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.", "version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.",
"video": "Video", "video": "Video",
"video_hover_setting": "Play video thumbnail on hover", "video_hover_setting": "Play video thumbnail on hover",

View file

@ -3,7 +3,8 @@ import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { TimeBucketSize, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; import { TimeBucketSize, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { writable, type Unsubscriber } from 'svelte/store'; import { t } from 'svelte-i18n';
import { get, writable, type Unsubscriber } from 'svelte/store';
import { handleError } from '../utils/handle-error'; import { handleError } from '../utils/handle-error';
import { websocketEvents } from './websocket'; import { websocketEvents } from './websocket';
@ -286,7 +287,8 @@ export class AssetStore {
this.emit(true); this.emit(true);
} catch (error) { } catch (error) {
handleError(error, 'Failed to load assets'); const $t = get(t);
handleError(error, $t('errors.failed_to_load_assets'));
} finally { } finally {
bucket.cancelToken = null; bucket.cancelToken = null;
} }

View file

@ -13,6 +13,8 @@ import {
type AssetMediaResponseDto, type AssetMediaResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { getServerErrorMessage, handleError } from './handle-error'; import { getServerErrorMessage, handleError } from './handle-error';
let _extensions: string[]; let _extensions: string[];
@ -83,6 +85,7 @@ function getDeviceAssetId(asset: File) {
async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: string): Promise<string | undefined> { async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: string): Promise<string | undefined> {
const fileCreatedAt = new Date(assetFile.lastModified).toISOString(); const fileCreatedAt = new Date(assetFile.lastModified).toISOString();
const deviceAssetId = getDeviceAssetId(assetFile); const deviceAssetId = getDeviceAssetId(assetFile);
const $t = get(t);
uploadAssetsStore.markStarted(deviceAssetId); uploadAssetsStore.markStarted(deviceAssetId);
@ -103,7 +106,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
let responseData: AssetMediaResponseDto | undefined; let responseData: AssetMediaResponseDto | undefined;
const key = getKey(); const key = getKey();
if (crypto?.subtle?.digest && !key) { if (crypto?.subtle?.digest && !key) {
uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Hashing...' }); uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_hashing') });
await tick(); await tick();
try { try {
const bytes = await assetFile.arrayBuffer(); const bytes = await assetFile.arrayBuffer();
@ -124,7 +127,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
} }
if (!responseData) { if (!responseData) {
uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Uploading...' }); uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_uploading') });
if (replaceAssetId) { if (replaceAssetId) {
const response = await uploadRequest<AssetMediaResponseDto>({ const response = await uploadRequest<AssetMediaResponseDto>({
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''), url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''),
@ -141,7 +144,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
}); });
if (![200, 201].includes(response.status)) { if (![200, 201].includes(response.status)) {
throw new Error('Failed to upload file'); throw new Error($t('errors.unable_to_upload_file'));
} }
responseData = response.data; responseData = response.data;
@ -155,9 +158,9 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
} }
if (albumId) { if (albumId) {
uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Adding to album...' }); uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_adding_to_album') });
await addAssetsToAlbum(albumId, [responseData.id]); await addAssetsToAlbum(albumId, [responseData.id]);
uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Added to album' }); uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_added_to_album') });
} }
uploadAssetsStore.updateAsset(deviceAssetId, { uploadAssetsStore.updateAsset(deviceAssetId, {
@ -170,7 +173,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
return responseData.id; return responseData.id;
} catch (error) { } catch (error) {
handleError(error, 'Unable to upload file'); handleError(error, $t('errors.unable_to_upload_file'));
const reason = getServerErrorMessage(error) || error; const reason = getServerErrorMessage(error) || error;
uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason }); uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
return; return;

View file

@ -331,9 +331,9 @@
return person; return person;
}); });
notificationController.show({ message: 'Date of birth saved successfully', type: NotificationType.Info }); notificationController.show({ message: $t('date_of_birth_saved'), type: NotificationType.Info });
} catch (error) { } catch (error) {
handleError(error, 'Unable to save date of birth'); handleError(error, $t('errors.unable_to_save_date_of_birth'));
} }
}; };

View file

@ -26,9 +26,11 @@
passwordRequired = false; passwordRequired = false;
isOwned = $user ? $user.id === sharedLink.userId : false; isOwned = $user ? $user.id === sharedLink.userId : false;
title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich'; title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich';
description = sharedLink.description || `${sharedLink.assets.length} shared photos & videos.`; description =
sharedLink.description ||
$t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } });
} catch (error) { } catch (error) {
handleError(error, 'Failed to get shared link'); handleError(error, $t('errors.unable_to_get_shared_link'));
} }
}; };
</script> </script>
@ -57,7 +59,7 @@
<div class="flex flex-col items-center justify-center mt-20"> <div class="flex flex-col items-center justify-center mt-20">
<div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('password_required')}</div> <div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('password_required')}</div>
<div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary"> <div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
Please enter the password to view this page. {$t('sharing_enter_password')}
</div> </div>
<div class="mt-4"> <div class="mt-4">
<form novalidate autocomplete="off" on:submit|preventDefault={handlePasswordSubmit}> <form novalidate autocomplete="off" on:submit|preventDefault={handlePasswordSubmit}>

View file

@ -78,7 +78,7 @@
try { try {
await loadConfig(); await loadConfig();
} catch (error) { } catch (error) {
handleError(error, 'Unable to connect to server'); handleError(error, $t('errors.unable_to_connect_to_server'));
} }
}); });
</script> </script>

View file

@ -59,16 +59,8 @@
return websocketEvents.on('on_user_delete', onDeleteSuccess); return websocketEvents.on('on_user_delete', onDeleteSuccess);
}); });
const deleteDateFormat: Intl.DateTimeFormatOptions = { const getDeleteDate = (deletedAt: string): Date => {
month: 'long', return DateTime.fromISO(deletedAt).plus({ days: $serverConfig.userDeleteDelay }).toJSDate();
day: 'numeric',
year: 'numeric',
};
const getDeleteDate = (deletedAt: string): string => {
return DateTime.fromISO(deletedAt)
.plus({ days: $serverConfig.userDeleteDelay })
.toLocaleString(deleteDateFormat, { locale: $locale });
}; };
const onUserCreated = async () => { const onUserCreated = async () => {
@ -245,7 +237,9 @@
{#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted} {#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted}
<CircleIconButton <CircleIconButton
icon={mdiDeleteRestore} icon={mdiDeleteRestore}
title="Restore user - scheduled removal on {getDeleteDate(immichUser.deletedAt)}" title={$t('admin.user_restore_scheduled_removal', {
values: { date: getDeleteDate(immichUser.deletedAt) },
})}
color="primary" color="primary"
size="16" size="16"
on:click={() => restoreUserHandler(immichUser)} on:click={() => restoreUserHandler(immichUser)}