mirror of
https://github.com/immich-app/immich.git
synced 2025-01-16 16:56:46 +01:00
chore(web): more translations for user settings and admin pages (#10161)
* chore(web): more translations for user settings and admin pages * JobSettings translations * feedback * missed one * feedback
This commit is contained in:
parent
0e1311e3d3
commit
9e5c52b7b7
34 changed files with 300 additions and 160 deletions
|
@ -76,13 +76,10 @@
|
|||
</div>
|
||||
|
||||
{#if forceDelete}
|
||||
<p class="text-immich-error">
|
||||
WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be
|
||||
recovered.
|
||||
</p>
|
||||
<p class="text-immich-error">{$t('admin.force_delete_user_warning')}</p>
|
||||
|
||||
<p class="immich-form-label text-sm" id="confirm-user-desc">
|
||||
To confirm, type "{user.email}" below
|
||||
{$t('admin.confirm_email_below', { values: { email: user.email } })}
|
||||
</p>
|
||||
|
||||
<input
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<Badge color="primary">
|
||||
<div class="flex flex-row gap-1">
|
||||
<span class="text-sm">
|
||||
{jobCounts.failed.toLocaleString($locale)} failed
|
||||
{$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })}
|
||||
</span>
|
||||
<CircleIconButton
|
||||
color="primary"
|
||||
|
@ -74,7 +74,7 @@
|
|||
{#if jobCounts.delayed > 0}
|
||||
<Badge color="secondary">
|
||||
<span class="text-sm">
|
||||
{jobCounts.delayed.toLocaleString($locale)} delayed
|
||||
{$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })}
|
||||
</span>
|
||||
</Badge>
|
||||
{/if}
|
||||
|
@ -119,12 +119,14 @@
|
|||
color="light-gray"
|
||||
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
|
||||
>
|
||||
<Icon path={mdiAlertCircle} size="36" /> DISABLED
|
||||
<Icon path={mdiAlertCircle} size="36" />
|
||||
{$t('disabled').toUpperCase()}
|
||||
</JobTileButton>
|
||||
{:else if !isIdle}
|
||||
{#if waitingCount > 0}
|
||||
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
|
||||
<Icon path={mdiClose} size="24" /> CLEAR
|
||||
<Icon path={mdiClose} size="24" />
|
||||
{$t('clear').toUpperCase()}
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
{#if queueStatus.isPaused}
|
||||
|
@ -134,14 +136,16 @@
|
|||
on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })}
|
||||
>
|
||||
<!-- size property is not reactive, so have to use width and height -->
|
||||
<Icon path={mdiFastForward} {size} /> RESUME
|
||||
<Icon path={mdiFastForward} {size} />
|
||||
{$t('resume').toUpperCase()}
|
||||
</JobTileButton>
|
||||
{:else}
|
||||
<JobTileButton
|
||||
color="light-gray"
|
||||
on:click={() => dispatch('command', { command: JobCommand.Pause, force: false })}
|
||||
>
|
||||
<Icon path={mdiPause} size="24" /> PAUSE
|
||||
<Icon path={mdiPause} size="24" />
|
||||
{$t('pause').toUpperCase()}
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
{:else if allowForceCommand}
|
||||
|
@ -161,7 +165,8 @@
|
|||
color="light-gray"
|
||||
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
|
||||
>
|
||||
<Icon path={mdiPlay} size="48" /> START
|
||||
<Icon path={mdiPlay} size="48" />
|
||||
{$t('start').toUpperCase()}
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -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 = <Partial<Record<JobName, 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 } }));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
{#if user.quotaSizeInBytes}
|
||||
({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%)
|
||||
{:else}
|
||||
(Unlimited)
|
||||
({$t('unlimited')})
|
||||
{/if}
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -270,7 +270,7 @@
|
|||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Disabled,
|
||||
text: $t('admin.disabled'),
|
||||
text: $t('disabled'),
|
||||
},
|
||||
]}
|
||||
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
||||
|
|
|
@ -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 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="{getJobName(jobName)} Concurrency"
|
||||
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
|
||||
desc=""
|
||||
bind:value={config.job[jobName].concurrency}
|
||||
required={true}
|
||||
|
@ -54,11 +55,11 @@
|
|||
{:else}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="{getJobName(jobName)} Concurrency"
|
||||
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
|
||||
desc=""
|
||||
value="1"
|
||||
disabled={true}
|
||||
title="This job is not concurrency-safe."
|
||||
title={$t('admin.job_not_concurrency_safe')}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -84,7 +84,9 @@
|
|||
|
||||
{#if $featureFlags.email}
|
||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email"> Send welcome email </label>
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email">
|
||||
{$t('admin.send_welcome_email')}
|
||||
</label>
|
||||
<Slider id="send-welcome-email" bind:checked={notify} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -101,7 +103,7 @@
|
|||
|
||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
|
||||
Require user to change password on first login
|
||||
{$t('admin.require_password_change_on_login')}
|
||||
</label>
|
||||
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
|
||||
</div>
|
||||
|
@ -113,9 +115,9 @@
|
|||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
|
||||
Quota Size (GiB)
|
||||
{$t('admin.quota_size_gib')}
|
||||
{#if quotaSizeWarning}
|
||||
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
|
||||
<p class="text-red-400 text-sm">{$t('admin.quota_higher_than_disk_size')}</p>
|
||||
{/if}
|
||||
</label>
|
||||
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
||||
|
|
|
@ -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 @@
|
|||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
|
||||
>Quota Size (GiB) {#if quotaSizeWarning}
|
||||
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
|
||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
|
||||
{$t('admin.quota_size_gib')}
|
||||
{#if quotaSizeWarning}
|
||||
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
|
||||
{/if}</label
|
||||
>
|
||||
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
||||
<p>Note: Enter 0 for unlimited quota</p>
|
||||
<p>{$t('admin.note_unlimited_quota')}</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
|
@ -130,10 +131,10 @@
|
|||
/>
|
||||
|
||||
<p>
|
||||
Note: To apply the Storage Label to previously uploaded assets, run the
|
||||
{$t('admin.note_apply_storage_label_previous_assets')}
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
|
||||
Storage Migration Job</a
|
||||
>
|
||||
{$t('admin.storage_template_migration_job')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -32,11 +32,9 @@
|
|||
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}>
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
|
||||
<p class="py-5 text-sm">
|
||||
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')}
|
||||
<br /><br />
|
||||
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')}
|
||||
</p>
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
|
||||
|
@ -50,7 +48,7 @@
|
|||
</div>
|
||||
<div class="mt-8 flex w-full gap-4">
|
||||
{#if isDuplicate}
|
||||
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
|
||||
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -33,9 +33,7 @@
|
|||
|
||||
<FullScreenModal {title} icon={mdiFolderSync} onClose={handleCancel}>
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form">
|
||||
<p class="py-5 text-sm">
|
||||
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.
|
||||
</p>
|
||||
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="path">{$t('path')}</label>
|
||||
|
@ -44,7 +42,7 @@
|
|||
|
||||
<div class="mt-8 flex w-full gap-4">
|
||||
{#if isDuplicate}
|
||||
<p class="text-red-500 text-sm">This import path already exists.</p>
|
||||
<p class="text-red-500 text-sm">{$t('admin.import_path_already_exists')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -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 @@
|
|||
>
|
||||
<td class="w-4/5 text-ellipsis px-4 text-sm">
|
||||
{#if importPaths.length === 0}
|
||||
No paths added
|
||||
{$t('admin.no_paths_added')}
|
||||
{/if}</td
|
||||
>
|
||||
<td class="w-1/5 text-ellipsis px-4 text-sm"
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
exclusionPatterns = library.exclusionPatterns;
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to add exclusion pattern');
|
||||
handleError(error, $t('errors.unable_to_add_exclusion_pattern'));
|
||||
} finally {
|
||||
exclusionPatternToAdd = '';
|
||||
addExclusionPattern = false;
|
||||
|
@ -74,7 +74,7 @@
|
|||
library.exclusionPatterns[editExclusionPattern] = editedExclusionPattern;
|
||||
exclusionPatterns = library.exclusionPatterns;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to edit exclude pattern');
|
||||
handleError(error, $t('errors.unable_to_edit_exclusion_pattern'));
|
||||
} finally {
|
||||
editExclusionPattern = null;
|
||||
}
|
||||
|
@ -94,7 +94,7 @@
|
|||
library.exclusionPatterns = library.exclusionPatterns.filter((path) => 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 @@
|
|||
>
|
||||
<td class="w-3/4 text-ellipsis px-4 text-sm">
|
||||
{#if exclusionPatterns.length === 0}
|
||||
No pattern added
|
||||
{$t('admin.no_pattern_added')}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="w-1/4 text-ellipsis px-4 text-sm"
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}>
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
|
||||
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
|
||||
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
|
||||
|
||||
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
||||
</form>
|
||||
|
|
|
@ -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 @@
|
|||
</div>
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="Play video thumbnail on hover"
|
||||
title={$t('video_hover_setting')}
|
||||
subtitle={$t('video_hover_setting_description')}
|
||||
bind:checked={$playVideoThumbnailOnHover}
|
||||
on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button type="submit" size="sm" on:click={() => handleSave()}>Save</Button>
|
||||
<Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -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}
|
||||
</div>
|
||||
|
@ -151,14 +151,18 @@
|
|||
<!-- I am sharing my assets with this user -->
|
||||
{#if partner.sharedByMe}
|
||||
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
|
||||
<p class="text-xs font-medium my-4">SHARED WITH {partner.user.name.toUpperCase()}</p>
|
||||
<p class="text-md">{partner.user.name} can access</p>
|
||||
<p class="text-xs font-medium my-4">
|
||||
{$t('shared_with_partner', { values: { partner: partner.user.name } }).toUpperCase()}
|
||||
</p>
|
||||
<p class="text-md">{$t('partner_can_access', { values: { partner: partner.user.name } })}</p>
|
||||
<ul class="text-sm">
|
||||
<li class="flex gap-2 place-items-center py-1 mt-2">
|
||||
<Icon path={mdiCheck} /> All your photos and videos except those in Archived and Deleted
|
||||
<Icon path={mdiCheck} />
|
||||
{$t('partner_can_access_assets')}
|
||||
</li>
|
||||
<li class="flex gap-2 place-items-center py-1">
|
||||
<Icon path={mdiCheck} /> The location where your photos were taken
|
||||
<Icon path={mdiCheck} />
|
||||
{$t('partner_can_access_location')}
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
|
@ -166,7 +170,9 @@
|
|||
<!-- this user is sharing assets with me -->
|
||||
{#if partner.sharedWithMe}
|
||||
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
|
||||
<p class="text-xs font-medium my-4">PHOTOS FROM {partner.user.name.toUpperCase()}</p>
|
||||
<p class="text-xs font-medium my-4">
|
||||
{$t('shared_from_partner', { values: { partner: partner.user.name } }).toUpperCase()}
|
||||
</p>
|
||||
<SettingSwitch
|
||||
title={$t('show_in_timeline')}
|
||||
subtitle={$t('show_in_timeline_setting_description')}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
const data = await createApiKey({ apiKeyCreateDto: detail });
|
||||
secret = data.secret;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to create a new API Key');
|
||||
handleError(error, $t('errors.unable_to_create_api_key'));
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
newKey = null;
|
||||
|
@ -48,11 +48,11 @@
|
|||
try {
|
||||
await updateApiKey({ id: editKey.id, apiKeyUpdateDto: { name: detail.name } });
|
||||
notificationController.show({
|
||||
message: `Saved API Key`,
|
||||
message: $t('saved_api_key'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save API Key');
|
||||
handleError(error, $t('errors.unable_to_save_api_key'));
|
||||
} finally {
|
||||
await refreshKeys();
|
||||
editKey = null;
|
||||
|
@ -62,7 +62,7 @@
|
|||
const handleDelete = async (key: ApiKeyResponseDto) => {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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 <noreply@immich.app>\"",
|
||||
"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",
|
||||
|
|
|
@ -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<T = unknown> {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
|
@ -115,26 +117,28 @@ export const downloadRequest = <TBody = unknown>(options: DownloadRequestOptions
|
|||
});
|
||||
};
|
||||
|
||||
export const getJobName = (jobName: JobName) => {
|
||||
const names: Record<JobName, string> = {
|
||||
[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, string> = {
|
||||
[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'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 @@
|
|||
<LinkButton>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiCog} size="18" />
|
||||
Manage Concurrency
|
||||
{$t('admin.manage_concurrency')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</a>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 @@
|
|||
}`}
|
||||
>
|
||||
<td class=" px-10 text-sm">
|
||||
<Icon path={mdiDatabase} size="40" title="External library (created on {library.createdAt})" />
|
||||
<Icon
|
||||
path={mdiDatabase}
|
||||
size="40"
|
||||
title={$t('admin.external_library_created_at', { values: { date: library.createdAt } })}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class=" text-ellipsis px-4 text-sm">{library.name}</td>
|
||||
|
@ -399,7 +403,7 @@
|
|||
{#if showContextMenu}
|
||||
<Portal target="body">
|
||||
<ContextMenu {...contextMenuPosition} onClose={() => onMenuExit()}>
|
||||
<MenuOption on:click={() => onRenameClicked()} text={`Rename`} />
|
||||
<MenuOption on:click={() => onRenameClicked()} text={$t('rename')} />
|
||||
|
||||
{#if selectedLibrary}
|
||||
<MenuOption on:click={() => onEditImportPathClicked()} text={$t('edit_import_paths')} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 @@
|
|||
<LinkButton on:click={() => handleRepair()} disabled={matches.length === 0 || repairing}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiWrench} size="18" />
|
||||
Repair All
|
||||
{$t('admin.repair_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiCheckAll} size="18" />
|
||||
Check All
|
||||
{$t('admin.check_all')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiDownload} size="18" />
|
||||
Export
|
||||
{$t('export')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => handleRefresh()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiRefresh} size="18" />
|
||||
Refresh
|
||||
{$t('refresh')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
@ -215,8 +221,8 @@
|
|||
<tr class="flex w-full place-items-center p-2 md:p-5">
|
||||
<th class="w-full text-sm place-items-center font-medium flex justify-between" colspan="2">
|
||||
<div class="px-3">
|
||||
<p>MATCHES {matches.length > 0 ? `(${matches.length})` : ''}</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mt-1">These files are matched by their checksums</p>
|
||||
<p>{$t('matches').toUpperCase()} {matches.length > 0 ? `(${matches.length})` : ''}</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mt-1">{$t('admin.these_files_matched_by_checksum')}</p>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -249,9 +255,9 @@
|
|||
<tr class="flex w-full place-items-center p-1 md:p-5">
|
||||
<th class="w-full text-sm font-medium justify-between place-items-center flex" colspan="2">
|
||||
<div class="px-3">
|
||||
<p>OFFLINE PATHS {orphans.length > 0 ? `(${orphans.length})` : ''}</p>
|
||||
<p>{$t('admin.offline_paths').toUpperCase()} {orphans.length > 0 ? `(${orphans.length})` : ''}</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mt-1">
|
||||
These results may be due to manual deletion of files that are not part of an external library.
|
||||
{$t('admin.offline_paths_description')}
|
||||
</p>
|
||||
</div>
|
||||
</th>
|
||||
|
@ -287,10 +293,9 @@
|
|||
<tr class="flex w-full place-items-center p-2 md:p-5">
|
||||
<th class="w-full text-sm font-medium place-items-center flex justify-between" colspan="2">
|
||||
<div class="px-3">
|
||||
<p>UNTRACKED FILES {extras.length > 0 ? `(${extras.length})` : ''}</p>
|
||||
<p>{$t('admin.untracked_files').toUpperCase()} {extras.length > 0 ? `(${extras.length})` : ''}</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mt-1">
|
||||
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')}
|
||||
</p>
|
||||
</div>
|
||||
</th>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -181,7 +181,7 @@
|
|||
<div class="flex flex-row items-center gap-2 bg-gray-100 p-3 dark:bg-gray-800">
|
||||
<Icon path={mdiAlert} class="text-yellow-400" size={18} />
|
||||
<h2 class="text-md text-immich-primary dark:text-immich-dark-primary">
|
||||
Config is currently set by a config file
|
||||
{$t('admin.config_set_by_file')}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -191,19 +191,19 @@
|
|||
<LinkButton on:click={() => copyToClipboard(JSON.stringify(config, null, 2))}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiContentCopy} size="18" />
|
||||
Copy to Clipboard
|
||||
{$t('copy_to_clipboard')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => downloadConfig()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiDownload} size="18" />
|
||||
Export as JSON
|
||||
{$t('export_as_json')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
<LinkButton on:click={() => inputElement?.click()}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiUpload} size="18" />
|
||||
Import from JSON
|
||||
{$t('import_from_json')}
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 @@
|
|||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>The user's password has been reset:</p>
|
||||
<p>{$t('admin.user_password_has_been_reset')}</p>
|
||||
|
||||
<div class="flex justify-center gap-2">
|
||||
<code
|
||||
|
@ -179,10 +179,7 @@
|
|||
</LinkButton>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Please provide the temporary password to the user and inform them they will need to change the password
|
||||
at their next login.
|
||||
</p>
|
||||
<p>{$t('admin.user_password_reset_description')}</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialog>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue