diff --git a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte index caca5c156a554..5d69e82a73692 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte @@ -76,13 +76,10 @@ {#if forceDelete} -

- WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be - recovered. -

+

{$t('admin.force_delete_user_warning')}

- To confirm, type "{user.email}" below + {$t('admin.confirm_email_below', { values: { email: user.email } })}

- {jobCounts.failed.toLocaleString($locale)} failed + {$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })} 0} - {jobCounts.delayed.toLocaleString($locale)} delayed + {$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })} {/if} @@ -119,12 +119,14 @@ color="light-gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: false })} > - DISABLED + + {$t('disabled').toUpperCase()} {:else if !isIdle} {#if waitingCount > 0} dispatch('command', { command: JobCommand.Empty, force: false })}> - CLEAR + + {$t('clear').toUpperCase()} {/if} {#if queueStatus.isPaused} @@ -134,14 +136,16 @@ on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })} > - RESUME + + {$t('resume').toUpperCase()} {:else} dispatch('command', { command: JobCommand.Pause, force: false })} > - PAUSE + + {$t('pause').toUpperCase()} {/if} {:else if allowForceCommand} @@ -161,7 +165,8 @@ color="light-gray" on:click={() => dispatch('command', { command: JobCommand.Start, force: false })} > - START + + {$t('start').toUpperCase()} {/if}
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index b49baad42319a..a27591ac26a49 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -43,7 +43,7 @@ if (dto.force) { const isConfirmed = await dialogController.show({ id: 'confirm-reprocess-all-faces', - prompt: 'Are you sure you want to reprocess all faces? This will also clear named people.', + prompt: $t('admin.confirm_reprocess_all_faces'), }); if (isConfirmed) { @@ -60,23 +60,23 @@ $: jobDetails = >>{ [JobName.ThumbnailGeneration]: { icon: mdiFileJpgBox, - title: getJobName(JobName.ThumbnailGeneration), + title: $getJobName(JobName.ThumbnailGeneration), subtitle: $t('admin.thumbnail_generation_job_description'), }, [JobName.MetadataExtraction]: { icon: mdiTable, - title: getJobName(JobName.MetadataExtraction), + title: $getJobName(JobName.MetadataExtraction), subtitle: $t('admin.metadata_extraction_job_description'), }, [JobName.Library]: { icon: mdiLibraryShelves, - title: getJobName(JobName.Library), + title: $getJobName(JobName.Library), subtitle: $t('admin.library_tasks_description'), allText: $t('all').toUpperCase(), missingText: $t('refresh').toUpperCase(), }, [JobName.Sidecar]: { - title: getJobName(JobName.Sidecar), + title: $getJobName(JobName.Sidecar), icon: mdiFileXmlBox, subtitle: $t('admin.sidecar_job_description'), allText: $t('sync').toUpperCase(), @@ -85,46 +85,44 @@ }, [JobName.SmartSearch]: { icon: mdiImageSearch, - title: getJobName(JobName.SmartSearch), + title: $getJobName(JobName.SmartSearch), subtitle: $t('admin.smart_search_job_description'), disabled: !$featureFlags.smartSearch, }, [JobName.DuplicateDetection]: { icon: mdiContentDuplicate, - title: getJobName(JobName.DuplicateDetection), + title: $getJobName(JobName.DuplicateDetection), subtitle: $t('admin.duplicate_detection_job_description'), disabled: !$featureFlags.duplicateDetection, }, [JobName.FaceDetection]: { icon: mdiFaceRecognition, - title: getJobName(JobName.FaceDetection), - subtitle: - 'Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. "All" (re-)processes all assets. "Missing" queues assets that haven\'t been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.', + title: $getJobName(JobName.FaceDetection), + subtitle: $t('admin.face_detection_description'), handleCommand: handleConfirmCommand, disabled: !$featureFlags.facialRecognition, }, [JobName.FacialRecognition]: { icon: mdiTagFaces, - title: getJobName(JobName.FacialRecognition), - subtitle: - 'Group detected faces into people. This step runs after Face Detection is complete. "All" (re-)clusters all faces. "Missing" queues faces that don\'t have a person assigned.', + title: $getJobName(JobName.FacialRecognition), + subtitle: $t('admin.facial_recognition_job_description'), handleCommand: handleConfirmCommand, disabled: !$featureFlags.facialRecognition, }, [JobName.VideoConversion]: { icon: mdiVideo, - title: getJobName(JobName.VideoConversion), + title: $getJobName(JobName.VideoConversion), subtitle: $t('admin.video_conversion_job_description'), }, [JobName.StorageTemplateMigration]: { icon: mdiFolderMove, - title: getJobName(JobName.StorageTemplateMigration), + title: $getJobName(JobName.StorageTemplateMigration), allowForceCommand: false, description: StorageMigrationDescription, }, [JobName.Migration]: { icon: mdiFolderMove, - title: getJobName(JobName.Migration), + title: $getJobName(JobName.Migration), subtitle: $t('admin.migration_job_description'), allowForceCommand: false, }, @@ -140,14 +138,14 @@ switch (jobCommand.command) { case JobCommand.Empty: { notificationController.show({ - message: `Cleared jobs for: ${title}`, + message: $t('admin.cleared_jobs', { values: { job: title } }), type: NotificationType.Info, }); break; } } } catch (error) { - handleError(error, `Command '${jobCommand.command}' failed for job: ${title}`); + handleError(error, $t('admin.failed_job_command', { values: { command: jobCommand.command, job: title } })); } } diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index 5a4c2f9c9c234..6842387df7882 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -110,7 +110,7 @@ {#if user.quotaSizeInBytes} ({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%) {:else} - (Unlimited) + ({$t('unlimited')}) {/if} diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 641b3a68ca0cc..f5e3370f819c7 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -53,7 +53,7 @@ } notificationController.show({ - message: 'Reset settings to the recent saved settings', + message: $t('admin.reset_settings_to_recent_saved'), type: NotificationType.Info, }); }; @@ -64,7 +64,7 @@ } notificationController.show({ - message: $t('reset_settings_to_default'), + message: $t('admin.reset_settings_to_default'), type: NotificationType.Info, }); }; diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index 7094d147ea190..ac2d354523921 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -270,7 +270,7 @@ }, { value: TranscodeHWAccel.Disabled, - text: $t('admin.disabled'), + text: $t('disabled'), }, ]} isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel} diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte index 3db53f749ba77..ed464fb26aa10 100644 --- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte +++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte @@ -9,6 +9,7 @@ import SettingInputField, { SettingInputFieldType, } from '$lib/components/shared-components/settings/setting-input-field.svelte'; + import { t } from 'svelte-i18n'; export let savedConfig: SystemConfigDto; export let defaultConfig: SystemConfigDto; @@ -45,7 +46,7 @@ {/if} diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 5f7dd08758594..f46685a5d938a 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -84,7 +84,9 @@ {#if $featureFlags.email}
- +
{/if} @@ -101,7 +103,7 @@
@@ -113,9 +115,9 @@
diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index f0d0c8ab78c96..15a03883ce5e5 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -55,7 +55,7 @@ const resetPassword = async () => { const isConfirmed = await dialogController.show({ id: 'confirm-reset-password', - prompt: `Are you sure you want to reset ${user.name}'s password?`, + prompt: $t('admin.confirm_user_password_reset', { values: { user: user.name } }), }); if (!isConfirmed) { @@ -110,13 +110,14 @@
-
@@ -130,10 +131,10 @@ />

- Note: To apply the Storage Label to previously uploaded assets, run the + {$t('admin.note_apply_storage_label_previous_assets')} - Storage Migration Job + {$t('admin.storage_template_migration_job')} +

diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte index da2b6f549f9e2..c09f1fbaf6bbb 100644 --- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte +++ b/web/src/lib/components/forms/library-exclusion-pattern-form.svelte @@ -32,11 +32,9 @@
handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">

- Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have - folders that contain files you don't want to import, such as RAW files. + {$t('admin.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/**". + {$t('admin.add_exclusion_pattern_description')}

@@ -50,7 +48,7 @@
{#if isDuplicate} -

This exclusion pattern already exists.

+

{$t('errors.exclusion_pattern_already_exists')}

{/if}
diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/components/forms/library-import-path-form.svelte index f1f36c704df3a..6d59e6ae9ea90 100644 --- a/web/src/lib/components/forms/library-import-path-form.svelte +++ b/web/src/lib/components/forms/library-import-path-form.svelte @@ -33,9 +33,7 @@
handleSubmit()} autocomplete="off" id="library-import-path-form"> -

- Specify a folder to import. This folder, including subfolders, will be scanned for images and videos. -

+

{$t('admin.library_import_path_description')}

@@ -44,7 +42,7 @@
{#if isDuplicate} -

This import path already exists.

+

{$t('admin.import_path_already_exists')}

{/if}
diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index b78c7984ccede..a2bb3a9686857 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -9,7 +9,6 @@ import type { ValidateLibraryImportPathResponseDto } from '@immich/sdk'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { s } from '$lib/utils'; import { t } from 'svelte-i18n'; export let library: LibraryResponseDto; @@ -54,13 +53,13 @@ if (failedPaths === 0) { if (notifyIfSuccessful) { notificationController.show({ - message: `All paths validated successfully`, + message: $t('admin.paths_validated_successfully'), type: NotificationType.Info, }); } } else { notificationController.show({ - message: `${failedPaths} path${s(failedPaths)} failed validation`, + message: $t('errors.paths_validation_failed', { values: { paths: failedPaths } }), type: NotificationType.Warning, }); } @@ -95,7 +94,7 @@ await revalidate(false); } } catch (error) { - handleError(error, 'Unable to add import path'); + handleError(error, $t('errors.unable_to_add_import_path')); } finally { addImportPath = false; importPathToAdd = null; @@ -121,7 +120,7 @@ } } catch (error) { editImportPath = null; - handleError(error, 'Unable to edit import path'); + handleError(error, $t('errors.unable_to_edit_import_path')); } finally { editImportPath = null; } @@ -141,7 +140,7 @@ library.importPaths = library.importPaths.filter((path) => path != pathToDelete); await handleValidation(); } catch (error) { - handleError(error, 'Unable to delete import path'); + handleError(error, $t('errors.unable_to_delete_import_path')); } finally { editImportPath = null; } @@ -230,7 +229,7 @@ > {#if importPaths.length === 0} - No paths added + {$t('admin.no_paths_added')} {/if} path != pathToDelete); exclusionPatterns = library.exclusionPatterns; } catch (error) { - handleError(error, 'Unable to delete exclude pattern'); + handleError(error, $t('errors.unable_to_delete_exclusion_pattern')); } finally { editExclusionPattern = null; } @@ -162,7 +162,7 @@ > {#if exclusionPatterns.length === 0} - No pattern added + {$t('admin.no_pattern_added')} {/if}
handleSubmit()} autocomplete="off" id="select-library-owner-form"> -

NOTE: This cannot be changed later!

+

{$t('admin.note_cannot_be_changed_later')}

diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index 7245fd5857937..9c53fd6a8ced3 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -17,6 +17,7 @@ import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; import { t, init } from 'svelte-i18n'; + import { invalidateAll } from '$app/navigation'; let time = new Date(); @@ -77,6 +78,7 @@ } await init({ fallbackLocale: defaultLang.code, initialLocale: newLang }); + await invalidateAll(); } }; @@ -143,7 +145,7 @@
($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)} diff --git a/web/src/lib/components/user-settings-page/change-password-settings.svelte b/web/src/lib/components/user-settings-page/change-password-settings.svelte index 5c8fa2b29c6f2..cf7bc0a022eef 100644 --- a/web/src/lib/components/user-settings-page/change-password-settings.svelte +++ b/web/src/lib/components/user-settings-page/change-password-settings.svelte @@ -32,7 +32,7 @@ } catch (error) { console.error('Error [user-profile] [changePassword]', error); notificationController.show({ - message: (error as HttpError)?.body?.message || 'Unable to change password', + message: (error as HttpError)?.body?.message || $t('errors.unable_to_change_password'), type: NotificationType.Error, }); } diff --git a/web/src/lib/components/user-settings-page/notifications-settings.svelte b/web/src/lib/components/user-settings-page/notifications-settings.svelte index ae43b58c0b3c2..6112f3efd6fe6 100644 --- a/web/src/lib/components/user-settings-page/notifications-settings.svelte +++ b/web/src/lib/components/user-settings-page/notifications-settings.svelte @@ -68,7 +68,7 @@
- +
diff --git a/web/src/lib/components/user-settings-page/oauth-settings.svelte b/web/src/lib/components/user-settings-page/oauth-settings.svelte index fd94fe811c6bf..10e71e64eb467 100644 --- a/web/src/lib/components/user-settings-page/oauth-settings.svelte +++ b/web/src/lib/components/user-settings-page/oauth-settings.svelte @@ -27,7 +27,7 @@ type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Unable to link OAuth account'); + handleError(error, $t('errors.unable_to_link_oauth_account')); } finally { await goto('?open=oauth'); } diff --git a/web/src/lib/components/user-settings-page/partner-settings.svelte b/web/src/lib/components/user-settings-page/partner-settings.svelte index 53524384cdc0b..f93ebfbb97134 100644 --- a/web/src/lib/components/user-settings-page/partner-settings.svelte +++ b/web/src/lib/components/user-settings-page/partner-settings.svelte @@ -79,8 +79,8 @@ const handleRemovePartner = async (partner: PartnerResponseDto) => { const isConfirmed = await dialogController.show({ id: 'remove-partner', - title: 'Stop sharing your photos?', - prompt: `${partner.name} will no longer be able to access your photos.`, + title: $t('stop_photo_sharing'), + prompt: $t('stop_photo_sharing_description', { values: { partner: partner.name } }), }); if (!isConfirmed) { @@ -115,7 +115,7 @@ partner.inTimeline = inTimeline; partners = partners; } catch (error) { - handleError(error, 'Unable to update timeline display status'); + handleError(error, $t('errors.unable_to_update_timeline_display_status')); } }; @@ -142,7 +142,7 @@ on:click={() => handleRemovePartner(partner.user)} icon={mdiClose} size={'16'} - title="Stop sharing your photos with this user" + title={$t('stop_sharing_photos_with_user')} /> {/if} @@ -151,14 +151,18 @@ {#if partner.sharedByMe}
-

SHARED WITH {partner.user.name.toUpperCase()}

-

{partner.user.name} can access

+

+ {$t('shared_with_partner', { values: { partner: partner.user.name } }).toUpperCase()} +

+

{$t('partner_can_access', { values: { partner: partner.user.name } })}

  • - All your photos and videos except those in Archived and Deleted + + {$t('partner_can_access_assets')}
  • - The location where your photos were taken + + {$t('partner_can_access_location')}
{/if} @@ -166,7 +170,9 @@ {#if partner.sharedWithMe}
-

PHOTOS FROM {partner.user.name.toUpperCase()}

+

+ {$t('shared_from_partner', { values: { partner: partner.user.name } }).toUpperCase()} +

{ const isConfirmed = await dialogController.show({ id: 'delete-api-key', - prompt: 'Are you sure you want to delete this API key?', + prompt: $t('delete_api_key_prompt'), }); if (!isConfirmed) { @@ -72,11 +72,11 @@ try { await deleteApiKey({ id: key.id }); notificationController.show({ - message: `Removed API Key: ${key.name}`, + message: $t('removed_api_key', { values: { name: key.name } }), type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Unable to remove API Key'); + handleError(error, $t('errors.unable_to_remove_api_key')); } finally { await refreshKeys(); } diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 48c50a9d60a85..85d088cc70077 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -21,12 +21,28 @@ "add_to_album": "Add to album", "add_to_shared_album": "Add to shared album", "admin": { + "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", + "background_task_job": "Background Tasks", + "check_all": "Check All", + "config_set_by_file": "Config is currently set by a config file", + "confirm_delete_library": "Are you sure you want to delete {library} library?", + "confirm_delete_library_assets": "Are you sure you want to delete this library? This will delete all {count} contained assets from Immich and cannot be undone. Files will remain on disk.", + "confirm_email_below": "To confirm, type \"{email}\" below", + "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.", + "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?", "crontab_guru": "Crontab Guru", "disable_login": "Disable login", - "disabled": "Disabled", "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", + "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", + "external_library_created_at": "External library (created on {date})", + "external_library_management": "External Library Management", + "face_detection": "Face Detection", + "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"All\" (re-)processes all assets. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.", + "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"All\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.", + "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.", + "forcing_refresh_library_files": "Forcing refresh of all library files", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", "image_prefer_embedded_preview": "Prefer embedded preview", "image_prefer_embedded_preview_setting_description": "Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.", @@ -42,10 +58,18 @@ "image_thumbnail_format": "Thumbnail format", "image_thumbnail_resolution": "Thumbnail resolution", "image_thumbnail_resolution_description": "Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.", + "job_concurrency": "{job} Concurrency", + "job_not_concurrency_safe": "This job is not concurrency-safe.", "job_settings": "Job Settings", "job_settings_description": "Manage job concurrency", + "job_status": "Job Status", + "jobs_delayed": "{jobCount} delayed", + "jobs_failed": "{jobCount} failed", + "library_created": "Created library: {library}", "library_cron_expression": "Cron Expression", "library_cron_expression_presets": "Cron Expression Presets", + "library_deleted": "Library deleted", + "library_import_path_description": "Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.", "library_scanning": "Periodic Scanning", "library_scanning_description": "Configure periodic library scanning", "library_scanning_enable_description": "Enable periodic library scanning", @@ -82,6 +106,7 @@ "machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings", "machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.", "machine_learning_url_description": "URL of the machine learning server", + "manage_concurrency": "Manage Concurrency", "manage_log_settings": "Manage log settings", "map_dark_style": "Dark style", "map_enable_description": "Enable map features", @@ -92,8 +117,15 @@ "map_settings": "Map & GPS Settings", "map_settings_description": "Manage map settings", "map_style_description": "URL to a style.json map theme", + "metadata_extraction_job": "Extract Metadata", "metadata_extraction_job_description": "Extract metadata information from each asset, such as GPS and resolution", + "migration_job": "Migration", "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", + "no_paths_added": "No paths added", + "no_pattern_added": "No pattern added", + "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", + "note_cannot_be_changed_later": "NOTE: This cannot be changed later!", + "note_unlimited_quota": "Note: Enter 0 for unlimited quota", "notification_email_from_address": "From address", "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \"", "notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)", @@ -131,28 +163,48 @@ "oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.", "oauth_storage_quota_default": "Default storage quota (GiB)", "oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).", + "offline_paths": "Offline Paths", + "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "password_enable_description": "Login with email and password", "password_settings": "Password Login", "password_settings_description": "Manage password login settings", + "paths_validated_successfully": "All paths validated successfully", + "quota_size_gib": "Quota Size (GiB)", + "refreshing_all_libraries": "Refreshing all libraries", + "removing_offline_files": "Removing Offline Files", + "repair_all": "Repair All", + "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", + "repaired_items": "Repaired {count, plural, one {# item} other {# items}}", + "require_password_change_on_login": "Require user to change password on first login", + "reset_settings_to_default": "Reset settings to default", + "reset_settings_to_recent_saved": "Reset settings to the recent saved settings", + "scanning_library_for_changed_files": "Scanning library for changed files", + "scanning_library_for_new_files": "Scanning library for new files", + "send_welcome_email": "Send welcome email", "server_external_domain_settings": "External domain", "server_external_domain_settings_description": "Domain for public shared links, including http(s)://", "server_settings": "Server Settings", "server_settings_description": "Manage server settings", "server_welcome_message": "Welcome Message", "server_welcome_message_description": "A message that is displayed on the login page.", + "sidecar_job": "Sidecar Metadata", "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_enable_description": "Enable storage template engine", "storage_template_hash_verification_enabled": "Hash verification failed", "storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications", + "storage_template_migration": "Storage Template Migration", "storage_template_migration_job": "Storage Migration Job", "storage_template_settings": "Storage template", "storage_template_settings_description": "Manage the folder structure and file name of the upload asset", + "system_settings": "System Settings", "theme_custom_css_settings": "Custom CSS", "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", "theme_settings_description": "Manage customization of the Immich web interface", + "these_files_matched_by_checksum": "These files are matched by their checksums", + "thumbnail_generation_job": "Generate Thumbnails", "thumbnail_generation_job_description": "Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person", "transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled).", "transcoding_acceleration_api": "Acceleration API", @@ -215,13 +267,20 @@ "trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them", "trash_settings": "Trash Settings", "trash_settings_description": "Manage trash settings", + "untracked_files": "Untracked Files", + "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "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_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_settings": "User Settings", "user_settings_description": "Manage user settings", + "user_successfully_removed": "User {email} has been successfully removed.", "version_check_enabled_description": "Enable periodic requests to GitHub to check for new releases", "version_check_settings": "Version Check", "version_check_settings_description": "Enable/disable the new version notification", + "video_conversion_job": "Transcode Videos", "video_conversion_job_description": "Transcode videos for wider compatibility with browsers and devices" }, "admin_email": "Admin Email", @@ -291,6 +350,7 @@ "context": "Context", "continue": "Continue", "copied_image_to_clipboard": "Copied image to clipboard.", + "copied_to_clipboard": "Copied to clipboard!", "copy_error": "Copy error", "copy_file_path": "Copy file path", "copy_image": "Copy Image", @@ -323,6 +383,7 @@ "default_locale_description": "Format dates and numbers based on your browser locale", "delete": "Delete", "delete_album": "Delete album", + "delete_api_key_prompt": "Are you sure you want to delete this API key?", "delete_key": "Delete key", "delete_library": "Delete library", "delete_link": "Delete link", @@ -332,6 +393,7 @@ "description": "Description", "details": "Details", "direction": "Direction", + "disabled": "Disabled", "disallow_edits": "Disallow edits", "discover": "Discover", "dismiss_all_errors": "Dismiss all errors", @@ -378,24 +440,39 @@ "error": "Error", "error_loading_image": "Error loading image", "errors": { + "cleared_jobs": "Cleared jobs for: {job}", + "exclusion_pattern_already_exists": "This exclusion pattern already exists.", + "failed_job_command": "Command {command} failed for job: {job}", + "import_path_already_exists": "This import path already exists.", + "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", + "quota_higher_than_disk_size": "You set a quota higher than the disk size", + "repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}", "unable_to_add_album_users": "Unable to add albums users", "unable_to_add_comment": "Unable to add comment", + "unable_to_add_exclusion_pattern": "Unable to add exclusion pattern", + "unable_to_add_import_path": "Unable to add import path", "unable_to_add_partners": "Unable to add partners", "unable_to_change_album_user_role": "Unable to change the album user's role", "unable_to_change_date": "Unable to change date", "unable_to_change_location": "Unable to change location", - "unable_to_check_item": "Unable to check item", - "unable_to_check_items": "Unable to check items", + "unable_to_change_password": "Unable to change password", + "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", "unable_to_create_library": "Unable to create library", "unable_to_create_user": "Unable to create user", "unable_to_delete_album": "Unable to delete album", "unable_to_delete_asset": "Unable to delete asset", + "unable_to_delete_exclusion_pattern": "Unable to delete exclusion pattern", + "unable_to_delete_import_path": "Unable to delete import path", "unable_to_delete_user": "Unable to delete user", + "unable_to_edit_exclusion_pattern": "Unable to edit exclusion pattern", + "unable_to_edit_import_path": "Unable to edit import path", "unable_to_empty_trash": "Unable to empty trash", "unable_to_enter_fullscreen": "Unable to enter fullscreen", "unable_to_exit_fullscreen": "Unable to exit fullscreen", "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", "unable_to_load_asset_activity": "Unable to load asset activity", "unable_to_load_items": "Unable to load items", @@ -403,8 +480,10 @@ "unable_to_play_video": "Unable to play video", "unable_to_refresh_user": "Unable to refresh user", "unable_to_remove_album_users": "Unable to remove albums users", + "unable_to_remove_api_key": "Unable to remove API Key", "unable_to_remove_comment": "Unable to remove comment", "unable_to_remove_library": "Unable to remove library", + "unable_to_remove_offline_files": "nable to remove offline files", "unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_reaction": "Unable to remove reaction", "unable_to_remove_user": "Unable to remove user", @@ -415,6 +494,7 @@ "unable_to_restore_trash": "Unable to restore trash", "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_name": "Unable to save name", "unable_to_save_profile": "Unable to save profile", "unable_to_save_settings": "Unable to save settings", @@ -427,6 +507,7 @@ "unable_to_update_library": "Unable to update library", "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" }, "every_day_at_onepm": "Every day at 1pm", @@ -438,6 +519,8 @@ "expire_after": "Expire after", "expired": "Expired", "explore": "Explore", + "export": "Export", + "export_as_json": "Export as JSON", "extension": "Extension", "external_libraries": "External Libraries", "failed_to_get_people": "Failed to get people", @@ -471,6 +554,7 @@ "image": "Image", "img": "Img", "immich_logo": "Immich Logo", + "import_from_json": "Import from JSON", "import_path": "Import path", "in_archive": "In archive", "include_archived": "Include archived", @@ -522,6 +606,7 @@ "map": "Map", "map_marker_with_image": "Map marker with image", "map_settings": "Map settings", + "matches": "Matches", "media_type": "Media type", "memories": "Memories", "memories_setting_description": "Manage what you see in your memories", @@ -579,6 +664,9 @@ "other_variables": "Other variables", "owned": "Owned", "owner": "Owner", + "partner_can_access": "{partner} can access", + "partner_can_access_assets": "All your photos and videos except those in Archived and Deleted", + "partner_can_access_location": "The location where your photos were taken", "partner_sharing": "Partner Sharing", "partners": "Partners", "password": "Password", @@ -635,6 +723,8 @@ "remove_from_favorites": "Remove from favorites", "remove_from_shared_link": "Remove from shared link", "remove_offline_files": "Remove Offline Files", + "removed_api_key": "Removed API Key: {name}", + "rename": "Rename", "repair": "Repair", "repair_no_results_message": "Untracked and missing files will show up here", "replace_with_upload": "Replace with upload", @@ -642,13 +732,14 @@ "reset": "Reset", "reset_password": "Reset password", "reset_people_visibility": "Reset people visibility", - "reset_settings_to_default": "Reset settings to default", "restore": "Restore", "restore_user": "Restore user", + "resume": "Resume", "retry_upload": "Retry upload", "review_duplicates": "Review duplicates", "role": "Role", "save": "Save", + "saved_api_key": "Saved API Key", "saved_profile": "Saved profile", "saved_settings": "Saved settings", "say_something": "Say something", @@ -696,7 +787,9 @@ "shared": "Shared", "shared_by": "Shared by", "shared_by_you": "Shared by you", + "shared_from_partner": "Photos from {partner}", "shared_links": "Shared links", + "shared_with_partner": "Shared with {partner}", "sharing": "Sharing", "sharing_sidebar_description": "Display a link to Sharing in the sidebar", "show_album_options": "Show album options", @@ -722,10 +815,14 @@ "stack": "Stack", "stack_selected_photos": "Stack selected photos", "stacktrace": "Stacktrace", + "start": "Start", "start_date": "Start date", "state": "State", "status": "Status", "stop_motion_photo": "Stop Motion Photo", + "stop_photo_sharing": "Stop sharing your photos?", + "stop_photo_sharing_description": "{partner} will no longer be able to access your photos.", + "stop_sharing_photos_with_user": "Stop sharing your photos with this user", "storage": "Storage", "storage_label": "Storage Label", "submit": "Submit", @@ -754,6 +851,7 @@ "unknown": "Unknown", "unknown_album": "Unknown Album", "unknown_year": "Unknown Year", + "unlimited": "Unlimited", "unlink_oauth": "Unlink Oauth", "unlinked_oauth_account": "Unlinked OAuth account", "unselect_all": "Unselect all", @@ -774,6 +872,7 @@ "variables": "Variables", "version": "Version", "video": "Video", + "video_hover_setting": "Play video thumbnail on hover", "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", "videos": "Videos", "view_all": "View All", diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index ccef17aee1be0..a100b709ae0df 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -18,6 +18,8 @@ import { type SharedLinkResponseDto, } from '@immich/sdk'; import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js'; +import { t } from 'svelte-i18n'; +import { derived, get } from 'svelte/store'; interface DownloadRequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -115,26 +117,28 @@ export const downloadRequest = (options: DownloadRequestOptions }); }; -export const getJobName = (jobName: JobName) => { - const names: Record = { - [JobName.ThumbnailGeneration]: 'Generate Thumbnails', - [JobName.MetadataExtraction]: 'Extract Metadata', - [JobName.Sidecar]: 'Sidecar Metadata', - [JobName.SmartSearch]: 'Smart Search', - [JobName.DuplicateDetection]: 'Duplicate Detection', - [JobName.FaceDetection]: 'Face Detection', - [JobName.FacialRecognition]: 'Facial Recognition', - [JobName.VideoConversion]: 'Transcode Videos', - [JobName.StorageTemplateMigration]: 'Storage Template Migration', - [JobName.Migration]: 'Migration', - [JobName.BackgroundTask]: 'Background Tasks', - [JobName.Search]: 'Search', - [JobName.Library]: 'Library', - [JobName.Notifications]: 'Notifications', - }; +export const getJobName = derived(t, ($t) => { + return (jobName: JobName) => { + const names: Record = { + [JobName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'), + [JobName.MetadataExtraction]: $t('admin.metadata_extraction_job'), + [JobName.Sidecar]: $t('admin.sidecar_job'), + [JobName.SmartSearch]: $t('admin.machine_learning_smart_search'), + [JobName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'), + [JobName.FaceDetection]: $t('admin.face_detection'), + [JobName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'), + [JobName.VideoConversion]: $t('admin.video_conversion_job'), + [JobName.StorageTemplateMigration]: $t('admin.storage_template_migration'), + [JobName.Migration]: $t('admin.migration_job'), + [JobName.BackgroundTask]: $t('admin.background_task_job'), + [JobName.Search]: $t('search'), + [JobName.Library]: $t('library'), + [JobName.Notifications]: $t('notifications'), + }; - return names[jobName]; -}; + return names[jobName]; + }; +}); let _key: string | undefined; let _sharedLink: SharedLinkResponseDto | undefined; @@ -222,11 +226,13 @@ export const getAssetJobIcon = (job: AssetJobName) => { }; export const copyToClipboard = async (secret: string) => { + const $t = get(t); + try { await navigator.clipboard.writeText(secret); - notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info }); + notificationController.show({ message: $t('copied_to_clipboard'), type: NotificationType.Info }); } catch (error) { - handleError(error, 'Cannot copy to clipboard, make sure you are accessing the page through https'); + handleError(error, $t('errors.unable_to_copy_to_clipboard')); } }; diff --git a/web/src/routes/(user)/user-settings/+page.ts b/web/src/routes/(user)/user-settings/+page.ts index eba40243e969a..cead4723f0d3b 100644 --- a/web/src/routes/(user)/user-settings/+page.ts +++ b/web/src/routes/(user)/user-settings/+page.ts @@ -1,5 +1,7 @@ import { authenticate } from '$lib/utils/auth'; import { getApiKeys, getSessions } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { @@ -7,12 +9,13 @@ export const load = (async () => { const keys = await getApiKeys(); const sessions = await getSessions(); + const $t = get(t); return { keys, sessions, meta: { - title: 'Settings', + title: $t('settings'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 269d528969f6f..c945b2a0396f2 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -8,6 +8,7 @@ import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk'; import { mdiCog } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; + import { t } from 'svelte-i18n'; import type { PageData } from './$types'; export let data: PageData; @@ -34,7 +35,7 @@
- Manage Concurrency + {$t('admin.manage_concurrency')}
diff --git a/web/src/routes/admin/jobs-status/+page.ts b/web/src/routes/admin/jobs-status/+page.ts index f3fba970a9c4f..1d5a445cd58c8 100644 --- a/web/src/routes/admin/jobs-status/+page.ts +++ b/web/src/routes/admin/jobs-status/+page.ts @@ -1,16 +1,19 @@ import { authenticate } from '$lib/utils/auth'; import { getAllJobsStatus } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); const jobs = await getAllJobsStatus(); + const $t = get(t); return { jobs, meta: { - title: 'Job Status', + title: $t('admin.job_status'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index ae4ee75c9e0ac..dc1e19681e9da 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -121,7 +121,7 @@ try { const createdLibrary = await createLibrary({ createLibraryDto: { ownerId } }); notificationController.show({ - message: `Created library: ${createdLibrary.name}`, + message: $t('admin.library_created', { values: { library: createdLibrary.name } }), type: NotificationType.Info, }); } catch (error) { @@ -159,7 +159,7 @@ try { await deleteLibrary({ id: deletedLibrary.id }); notificationController.show({ - message: `Library deleted`, + message: $t('admin.library_deleted'), type: NotificationType.Info, }); } catch (error) { @@ -177,7 +177,7 @@ await scanLibrary({ id: library.id, scanLibraryDto: {} }); } notificationController.show({ - message: `Refreshing all libraries`, + message: $t('admin.refreshing_all_libraries'), type: NotificationType.Info, }); } catch (error) { @@ -189,7 +189,7 @@ try { await scanLibrary({ id: libraryId, scanLibraryDto: {} }); notificationController.show({ - message: `Scanning library for new files`, + message: $t('admin.scanning_library_for_new_files'), type: NotificationType.Info, }); } catch (error) { @@ -201,7 +201,7 @@ try { await scanLibrary({ id: libraryId, scanLibraryDto: { refreshModifiedFiles: true } }); notificationController.show({ - message: `Scanning library for changed files`, + message: $t('admin.scanning_library_for_changed_files'), type: NotificationType.Info, }); } catch (error) { @@ -213,7 +213,7 @@ try { await scanLibrary({ id: libraryId, scanLibraryDto: { refreshAllFiles: true } }); notificationController.show({ - message: `Forcing refresh of all library files`, + message: $t('admin.forcing_refresh_library_files'), type: NotificationType.Info, }); } catch (error) { @@ -225,11 +225,11 @@ try { await removeOfflineFiles({ id: libraryId }); notificationController.show({ - message: `Removing Offline Files`, + message: $t('admin.removing_offline_files'), type: NotificationType.Info, }); } catch (error) { - handleError(error, 'Unable to remove offline files'); + handleError(error, $t('errors.unable_to_remove_offline_files')); } }; @@ -289,7 +289,7 @@ const isConfirmedLibrary = await dialogController.show({ id: 'delete-library', - prompt: `Are you sure you want to delete ${selectedLibrary.name} library?`, + prompt: $t('admin.confirm_delete_library', { values: { library: selectedLibrary.name } }), }); if (!isConfirmedLibrary) { @@ -302,7 +302,7 @@ const isConfirmedLibraryAssetCount = await dialogController.show({ id: 'delete-library-assets', - prompt: `Are you sure you want to delete this library? This will delete all ${deleteAssetCount} contained assets from Immich and cannot be undone. Files will remain on disk.`, + prompt: $t('admin.confirm_delete_library_assets', { values: { count: deleteAssetCount } }), }); if (!isConfirmedLibraryAssetCount) { @@ -366,7 +366,11 @@ }`} > - + {library.name} @@ -399,7 +403,7 @@ {#if showContextMenu} onMenuExit()}> - onRenameClicked()} text={`Rename`} /> + onRenameClicked()} text={$t('rename')} /> {#if selectedLibrary} onEditImportPathClicked()} text={$t('edit_import_paths')} /> diff --git a/web/src/routes/admin/library-management/+page.ts b/web/src/routes/admin/library-management/+page.ts index 5cb90cd7f9850..b7d86666a2f0f 100644 --- a/web/src/routes/admin/library-management/+page.ts +++ b/web/src/routes/admin/library-management/+page.ts @@ -1,16 +1,19 @@ import { authenticate, requestServerInfo } from '$lib/utils/auth'; import { searchUsersAdmin } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); await requestServerInfo(); const allUsers = await searchUsersAdmin({ withDeleted: false }); + const $t = get(t); return { allUsers, meta: { - title: 'External Library Management', + title: $t('admin.external_library_management'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte index e60d521cddf0b..3d057b40549cc 100644 --- a/web/src/routes/admin/repair/+page.svelte +++ b/web/src/routes/admin/repair/+page.svelte @@ -80,7 +80,7 @@ notificationController.show({ type: NotificationType.Info, - message: `Repaired ${matches.length} items`, + message: $t('admin.repaired_items', { values: { count: matches.length } }), }); matches = []; @@ -118,10 +118,13 @@ try { const matched = await loadAndMatch([filename]); if (matched) { - notificationController.show({ message: `Matched 1 item`, type: NotificationType.Info }); + notificationController.show({ + message: $t('admin.repair_matched_items', { values: { count: 1 } }), + type: NotificationType.Info, + }); } } catch (error) { - handleError(error, $t('errors.unable_to_check_item')); + handleError(error, $t('errors.repair_unable_to_check_items', { values: { count: 'one' } })); } }; @@ -137,12 +140,15 @@ count += await loadAndMatch(filenames.slice(index, index + chunkSize)); } } catch (error) { - handleError(error, $t('errors.unable_to_check_items')); + handleError(error, $t('errors.repair_unable_to_check_items', { values: { count: 'other' } })); } finally { checking = false; } - notificationController.show({ message: `Matched ${count} items`, type: NotificationType.Info }); + notificationController.show({ + message: $t('admin.repair_matched_items', { values: { count } }), + type: NotificationType.Info, + }); }; const loadAndMatch = async (filenames: string[]) => { @@ -178,25 +184,25 @@ handleRepair()} disabled={matches.length === 0 || repairing}>
- Repair All + {$t('admin.repair_all')}
handleCheckAll()} disabled={extras.length === 0 || checking}>
- Check All + {$t('admin.check_all')}
handleDownload()} disabled={extras.length + orphans.length === 0}>
- Export + {$t('export')}
handleRefresh()}>
- Refresh + {$t('refresh')}
@@ -215,8 +221,8 @@
-

MATCHES {matches.length > 0 ? `(${matches.length})` : ''}

-

These files are matched by their checksums

+

{$t('matches').toUpperCase()} {matches.length > 0 ? `(${matches.length})` : ''}

+

{$t('admin.these_files_matched_by_checksum')}

@@ -249,9 +255,9 @@
-

OFFLINE PATHS {orphans.length > 0 ? `(${orphans.length})` : ''}

+

{$t('admin.offline_paths').toUpperCase()} {orphans.length > 0 ? `(${orphans.length})` : ''}

- These results may be due to manual deletion of files that are not part of an external library. + {$t('admin.offline_paths_description')}

@@ -287,10 +293,9 @@
-

UNTRACKED FILES {extras.length > 0 ? `(${extras.length})` : ''}

+

{$t('admin.untracked_files').toUpperCase()} {extras.length > 0 ? `(${extras.length})` : ''}

- These files are not tracked by the application. They can be the results of failed moves, - interrupted uploads, or left behind due to a bug + {$t('admin.untracked_files_description')}

diff --git a/web/src/routes/admin/repair/+page.ts b/web/src/routes/admin/repair/+page.ts index 17248d47c919a..119f4b1698a8d 100644 --- a/web/src/routes/admin/repair/+page.ts +++ b/web/src/routes/admin/repair/+page.ts @@ -1,16 +1,19 @@ import { authenticate } from '$lib/utils/auth'; import { getAuditFiles } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); const { orphans, extras } = await getAuditFiles(); + const $t = get(t); return { orphans, extras, meta: { - title: 'Repair', + title: $t('repair'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/admin/server-status/+page.ts b/web/src/routes/admin/server-status/+page.ts index 75116dd3bdf3a..427cdfe67f33f 100644 --- a/web/src/routes/admin/server-status/+page.ts +++ b/web/src/routes/admin/server-status/+page.ts @@ -1,15 +1,18 @@ import { authenticate } from '$lib/utils/auth'; import { getServerStatistics } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); const stats = await getServerStatistics(); + const $t = get(t); return { stats, meta: { - title: 'Server Stats', + title: $t('server_stats'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 34c6d7e5cc425..e1cbdc534c112 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -181,7 +181,7 @@

- Config is currently set by a config file + {$t('admin.config_set_by_file')}

{/if} @@ -191,19 +191,19 @@ copyToClipboard(JSON.stringify(config, null, 2))}>
- Copy to Clipboard + {$t('copy_to_clipboard')}
downloadConfig()}>
- Export as JSON + {$t('export_as_json')}
inputElement?.click()}>
- Import from JSON + {$t('import_from_json')}
diff --git a/web/src/routes/admin/system-settings/+page.ts b/web/src/routes/admin/system-settings/+page.ts index 144ac2af90604..1274f2316f68e 100644 --- a/web/src/routes/admin/system-settings/+page.ts +++ b/web/src/routes/admin/system-settings/+page.ts @@ -1,15 +1,18 @@ import { authenticate } from '$lib/utils/auth'; import { getConfig } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); const configs = await getConfig(); + const $t = get(t); return { configs, meta: { - title: 'System Settings', + title: $t('admin.system_settings'), }, }; }) satisfies PageLoad; diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index bc44e52f69c48..e74ddb960688f 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -48,7 +48,7 @@ allUsers = allUsers.filter((user) => user.id !== userId); notificationController.show({ type: NotificationType.Info, - message: `User ${user.email} has been successfully removed.`, + message: $t('admin.user_successfully_removed', { values: { email: user.email } }), }); } }; @@ -164,7 +164,7 @@ >
-

The user's password has been reset:

+

{$t('admin.user_password_has_been_reset')}

-

- Please provide the temporary password to the user and inform them they will need to change the password - at their next login. -

+

{$t('admin.user_password_reset_description')}

diff --git a/web/src/routes/admin/user-management/+page.ts b/web/src/routes/admin/user-management/+page.ts index 4c62e36c9151f..5fdee6d455672 100644 --- a/web/src/routes/admin/user-management/+page.ts +++ b/web/src/routes/admin/user-management/+page.ts @@ -1,16 +1,19 @@ import { authenticate, requestServerInfo } from '$lib/utils/auth'; import { searchUsersAdmin } from '@immich/sdk'; +import { t } from 'svelte-i18n'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); await requestServerInfo(); const allUsers = await searchUsersAdmin({ withDeleted: true }); + const $t = get(t); return { allUsers, meta: { - title: 'User Management', + title: $t('admin.user_management'), }, }; }) satisfies PageLoad;