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">
<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"
bind:checked={forceDelete}
on:change={() => {

View file

@ -51,7 +51,7 @@
>
<svelte:fragment slot="prompt">
<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>
<FormatMessage key="admin.authentication_settings_reenable" let:message>
<a

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -209,7 +209,7 @@
</div>
{:else}
{#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
role="button"
@ -261,8 +261,8 @@
curve
shadow
url="/src/lib/assets/no-thumbnail.png"
altText="Unassigned"
title="Unassigned"
altText={$t('face_unassigned')}
title={$t('face_unassigned')}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
@ -273,8 +273,8 @@
curve
shadow
url={data === null ? '/src/lib/assets/no-thumbnail.png' : data}
altText="Unassigned"
title="Unassigned"
altText={$t('face_unassigned')}
title={$t('face_unassigned')}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
@ -289,7 +289,7 @@
{#if selectedPersonToReassign[face.id]?.id}
{selectedPersonToReassign[face.id]?.name}
{:else}
<span class={personName == 'Unassigned' ? 'dark:text-gray-500' : ''}>{personName}</span>
<span class={personName === $t('face_unassigned') ? 'dark:text-gray-500' : ''}>{personName}</span>
{/if}
</p>
{/if}
@ -322,7 +322,7 @@
<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"
>
<Icon color="primary" path={mdiAccountOff} ariaLabel="Just a face" size="18" />
<Icon color="primary" path={mdiAccountOff} ariaHidden size="18" />
</div>
{/if}
</div>

View file

@ -57,7 +57,12 @@
<div class="dark:text-immich-dark-fg">
<div
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">
<Icon path={mdiChartPie} size="24" />

View file

@ -67,7 +67,7 @@
{: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]">
Skipped
{$t('asset_skipped')}
{#if uploadAsset.message}
({uploadAsset.message})
{/if}
@ -75,7 +75,7 @@
{:else if uploadAsset.state === UploadState.DONE}
<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]">
Uploaded
{$t('asset_uploaded')}
{#if uploadAsset.message}
({uploadAsset.message})
{/if}

View file

@ -5,6 +5,7 @@
<script lang="ts">
import { getProfileImageUrl } from '$lib/utils';
import { type UserAvatarColor } from '@immich/sdk';
import { t } from 'svelte-i18n';
interface User {
id: string;
@ -77,7 +78,7 @@
<img
bind:this={img}
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:hidden={showFallback}
draggable="false"

View file

@ -50,7 +50,7 @@
</FormatMessage>
</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">
<code>{$t('server_version')}: {serverVersion}</code>

View file

@ -51,7 +51,7 @@
<div class="flex flex-col justify-center gap-1 dark:text-white">
<span class="text-sm">
{#if device.deviceType || device.deviceOS}
<span>{device.deviceOS || 'Unknown'}{device.deviceType || 'Unknown'}</span>
<span>{device.deviceOS || $t('unknown')}{device.deviceType || $t('unknown')}</span>
{:else}
<span>{$t('unknown')}</span>
{/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/**\".",
"authentication_settings": "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>.",
"background_task_job": "Background Tasks",
"check_all": "Check All",
@ -125,6 +126,7 @@
"map_dark_style": "Dark style",
"map_enable_description": "Enable map features",
"map_light_style": "Light style",
"map_manage_reverse_geocoding_settings": "Manage <link>Reverse Geocoding</link> settings",
"map_reverse_geocoding": "Reverse Geocoding",
"map_reverse_geocoding_enable_description": "Enable reverse geocoding",
"map_reverse_geocoding_settings": "Reverse Geocoding Settings",
@ -209,6 +211,8 @@
"sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem",
"slideshow_duration_description": "Number of seconds to display each image",
"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_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",
@ -298,10 +302,12 @@
"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_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_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_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_description": "Manage user settings",
"user_successfully_removed": "User {email} has been successfully removed.",
@ -358,10 +364,17 @@
"archived_count": "{count, plural, other {Archived #}}",
"are_these_the_same_person": "Are these the same person?",
"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_has_unassigned_faces": "Asset has unassigned faces",
"asset_hashing": "Hashing...",
"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_skipped": "Skipped",
"asset_uploaded": "Uploaded",
"asset_uploading": "Uploading...",
"assets": "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",
@ -456,6 +469,7 @@
"date_after": "Date after",
"date_and_time": "Date and Time",
"date_before": "Date before",
"date_of_birth_saved": "Date of birth saved successfully",
"date_range": "Date range",
"day": "Day",
"deduplicate_all": "Deduplicate All",
@ -544,6 +558,8 @@
"failed_to_create_shared_link": "Failed to create shared link",
"failed_to_edit_shared_link": "Failed to edit shared link",
"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_unstack_assets": "Failed to un-stack assets",
"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_complete_oauth_login": "Unable to complete OAuth login",
"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_create_admin_account": "Unable to create admin account",
"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_exit_fullscreen": "Unable to exit fullscreen",
"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_link_oauth_account": "Unable to link OAuth account",
"unable_to_load_album": "Unable to load album",
@ -616,6 +634,7 @@
"unable_to_restore_user": "Unable to restore user",
"unable_to_save_album": "Unable to save album",
"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_profile": "Unable to save profile",
"unable_to_save_settings": "Unable to save settings",
@ -632,7 +651,8 @@
"unable_to_update_location": "Unable to update location",
"unable_to_update_settings": "Unable to update settings",
"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",
"exit_slideshow": "Exit Slideshow",
@ -646,6 +666,7 @@
"extension": "Extension",
"external": "External",
"external_libraries": "External Libraries",
"face_unassigned": "Unassigned",
"favorite": "Favorite",
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites",
@ -870,6 +891,7 @@
"previous_memory": "Previous memory",
"previous_or_next_photo": "Previous or next photo",
"primary": "Primary",
"profile_image_of_user": "Profile image of {title}",
"profile_picture_set": "Profile picture set.",
"public_album": "Public album",
"public_share": "Public Share",
@ -991,6 +1013,7 @@
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
"shared_with_partner": "Shared with {partner}",
"sharing": "Sharing",
"sharing_enter_password": "Please enter the password to view this page.",
"sharing_sidebar_description": "Display a link to Sharing in the sidebar",
"shift_to_permanent_delete": "press ⇧ to permanently delete asset",
"show_album_options": "Show album options",
@ -1107,6 +1130,7 @@
"validate": "Validate",
"variables": "Variables",
"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.",
"video": "Video",
"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 { throttle } from 'lodash-es';
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 { websocketEvents } from './websocket';
@ -286,7 +287,8 @@ export class AssetStore {
this.emit(true);
} catch (error) {
handleError(error, 'Failed to load assets');
const $t = get(t);
handleError(error, $t('errors.failed_to_load_assets'));
} finally {
bucket.cancelToken = null;
}

View file

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

View file

@ -331,9 +331,9 @@
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) {
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;
isOwned = $user ? $user.id === sharedLink.userId : false;
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) {
handleError(error, 'Failed to get shared link');
handleError(error, $t('errors.unable_to_get_shared_link'));
}
};
</script>
@ -57,7 +59,7 @@
<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="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 class="mt-4">
<form novalidate autocomplete="off" on:submit|preventDefault={handlePasswordSubmit}>

View file

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

View file

@ -59,16 +59,8 @@
return websocketEvents.on('on_user_delete', onDeleteSuccess);
});
const deleteDateFormat: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
year: 'numeric',
};
const getDeleteDate = (deletedAt: string): string => {
return DateTime.fromISO(deletedAt)
.plus({ days: $serverConfig.userDeleteDelay })
.toLocaleString(deleteDateFormat, { locale: $locale });
const getDeleteDate = (deletedAt: string): Date => {
return DateTime.fromISO(deletedAt).plus({ days: $serverConfig.userDeleteDelay }).toJSDate();
};
const onUserCreated = async () => {
@ -245,7 +237,9 @@
{#if immichUser.deletedAt && immichUser.status === UserStatus.Deleted}
<CircleIconButton
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"
size="16"
on:click={() => restoreUserHandler(immichUser)}