chore: i18n pass, update progress bar

This commit is contained in:
izzy
2025-11-25 11:07:11 +00:00
parent 1cdffeb3be
commit 390f0b2817
5 changed files with 113 additions and 61 deletions

View File

@@ -177,10 +177,17 @@
"machine_learning_smart_search_enabled": "Enable smart search",
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
"maintenance_restore_backup": "Restore Backup",
"maintenance_delete_backup": "Delete Backup",
"maintenance_delete_backup_description": "This file will be irrevocably deleted.",
"maintenance_restore_backup_description": "Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.",
"maintenance_settings": "Maintenance",
"maintenance_settings_description": "Put Immich into maintenance mode.",
"maintenance_start": "Start maintenance mode",
"maintenance_upload_backup": "Upload database backup file",
"maintenance_start_error": "Failed to start maintenance mode.",
"maintenance_delete_error": "Failed to delete backup.",
"maintenance_upload_backup_error": "Could not upload backup, is it an .sql/.sql.gz file?",
"manage_concurrency": "Manage Concurrency",
"manage_log_settings": "Manage log settings",
"map_dark_style": "Dark style",
@@ -1324,12 +1331,26 @@
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
"main_menu": "Main menu",
"maintenance_action_restore": "Restoring Database",
"maintenance_description": "Immich has been put into <link>maintenance mode</link>.",
"maintenance_end": "End maintenance mode",
"maintenance_end_error": "Failed to end maintenance mode.",
"maintenance_logged_in_as": "Currently logged in as {user}",
"maintenance_task_backup": "Creating a backup of the existing database...",
"maintenance_task_restore": "Restoring the chosen backup...",
"maintenance_restore_from_backup": "Restore From Backup",
"maintenance_restore_library": "Restore Your Library",
"maintenance_restore_library_confirm": "If this looks correct, continue to restoring a backup!",
"maintenance_restore_library_description": "Restoring Database",
"maintenance_restore_library_folder_has_files": "{folder} has {count} folder(s)",
"maintenance_restore_library_folder_no_files": "{folder} is missing files!",
"maintenance_restore_library_folder_pass": "readable and writable",
"maintenance_restore_library_folder_read_fail": "not readable",
"maintenance_restore_library_folder_write_fail": "not writable",
"maintenance_restore_library_hint_missing_files": "You may be missing important files",
"maintenance_restore_library_hint_regenerate_later": "You can regenerate these later in settings",
"maintenance_restore_library_hint_storage_template_missing_files": "Using storage template? You may be missing files",
"maintenance_restore_library_loading": "Loading integrity checks and heuristics…",
"maintenance_task_backup": "Creating a backup of the existing database…",
"maintenance_task_restore": "Restoring the chosen backup…",
"maintenance_title": "Temporarily Unavailable",
"make": "Make",
"manage_geolocation": "Manage location",
@@ -1916,6 +1937,12 @@
"shared_link_edit_expire_after_option_hours": "{count} hours",
"shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{count} minutes",
"created_day_ago": "Created 1 day ago",
"created_days_ago": "Created {count} days ago",
"created_hour_ago": "Created 1 hour ago",
"created_hours_ago": "Created {count} hours ago",
"created_minute_ago": "Created 1 minute ago",
"created_minutes_ago": "Created {count} minutes ago",
"shared_link_edit_expire_after_option_months": "{count} months",
"shared_link_edit_expire_after_option_year": "{count} year",
"shared_link_edit_password_hint": "Enter the share password",

View File

@@ -18,6 +18,7 @@
IconButton,
menuManager,
modalManager,
ProgressBar,
Stack,
Text,
type ContextMenuBaseProps,
@@ -36,15 +37,15 @@
function mapBackups(filenames: string[]) {
return filenames.map((filename) => {
const date = /(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/.exec(filename);
const hoursAgo = date
const minutesAgo = date
? Math.floor(
(+Date.now() - +new Date(`${date[1]}-${date[2]}-${date[3]}T${date[4]}:${date[5]}:${date[6]}`)) / 36e5,
(+Date.now() - +new Date(`${date[1]}-${date[2]}-${date[3]}T${date[4]}:${date[5]}:${date[6]}`)) / 60_000,
)
: null;
return {
filename,
hoursAgo,
minutesAgo,
};
});
}
@@ -61,9 +62,9 @@
async function restore(filename: string) {
const confirm = await modalManager.showDialog({
confirmText: 'Restore',
title: 'Restore Backup',
prompt: 'Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.',
confirmText: $t('restore'),
title: $t('admin.maintenance_restore_backup'),
prompt: $t('admin.maintenance_restore_backup_description'),
});
if (confirm) {
@@ -82,9 +83,9 @@
async function remove(filename: string) {
const confirm = await modalManager.showDialog({
confirmText: 'Delete',
title: 'Delete Backup',
prompt: 'This file will be irrevocably deleted.',
confirmText: $t('delete'),
title: $t('admin.maintenance_delete_backup'),
prompt: $t('admin.maintenance_delete_backup_description'),
});
if (confirm) {
@@ -97,7 +98,7 @@
backups = backups.filter((backup) => backup.filename !== filename);
} catch (error) {
handleError(error, 'failed to delete backup i18n');
handleError(error, $t('admin.maintenance_delete_error'));
} finally {
deleting.delete(filename);
}
@@ -114,14 +115,14 @@
target: event.currentTarget as HTMLElement,
items: [
{
title: 'Download',
title: $t('download'),
icon: mdiDownload,
onSelect() {
void download(filename);
},
},
{
title: 'Delete',
title: $t('delete'),
icon: mdiTrashCanOutline,
color: 'danger',
onSelect() {
@@ -153,7 +154,7 @@
const { backups: newList } = await listBackups();
backups = mapBackups(newList);
} catch (error) {
handleError(error, 'Could not upload backup, is it an .sql/.sql.gz file?');
handleError(error, $t('admin.maintenance_upload_backup_error'));
} finally {
uploadProgress = -1;
}
@@ -165,15 +166,13 @@
<CardBody>
{#if uploadProgress === -1}
<HStack>
<Text class="grow">Upload database backup file</Text>
<Button size="small" onclick={upload}>Select file</Button>
<Text class="grow">{$t('admin.maintenance_upload_backup')}</Text>
<Button size="tiny" onclick={upload}>{$t('select_from_computer')}</Button>
</HStack>
{:else}
<HStack>
<Text class="grow">Uploading...</Text>
<div class="grow h-2.5 bg-gray-300 rounded-full overflow-hidden">
<div class="h-full bg-blue-600 transition-all duration-700" style="width: {uploadProgress * 100}%"></div>
</div>
<HStack gap={8}>
<Text class="grow">{$t('asset_uploading')}</Text>
<ProgressBar progress={uploadProgress} size="tiny" />
</HStack>
{/if}
</CardBody>
@@ -185,20 +184,48 @@
<HStack>
<Stack class="grow">
<Text>{backup.filename}</Text>
{#if typeof backup.hoursAgo === 'number'}
{#if backup.hoursAgo <= 24}
{#if typeof backup.minutesAgo === 'number'}
<Text color="info" size="small">
{#if backup.minutesAgo <= 1}
{$t('created_minute_ago')}
{:else if backup.minutesAgo < 60}
{$t('created_minutes_ago', {
values: {
count: backup.minutesAgo,
},
})}
{:else if backup.minutesAgo < 60 * 2}
{$t('created_hour_ago')}
{:else if backup.minutesAgo < 60 * 24}
{$t('created_hours_ago', {
values: {
count: Math.floor(backup.minutesAgo / 60),
},
})}
{:else if backup.minutesAgo < 60 * 24 * 2}
{$t('created_day_ago')}
{:else}
{$t('created_days_ago', {
values: {
count: Math.floor(backup.minutesAgo / (60 * 24)),
},
})}
{/if}
</Text>
<!-- {#if backup.hoursAgo <= 24}
<Text color="info" size="small">Created {backup.hoursAgo} hours ago</Text>
{:else if backup.hoursAgo <= 48}
<Text color="info" size="small">Created 1 day ago</Text>
{:else}
<Text color="info" size="small">Created {Math.floor(backup.hoursAgo / 24)} days ago</Text>
{/if}
{/if} -->
{/if}
</Stack>
<Button size="small" disabled={deleting.has(backup.filename)} onclick={() => restore(backup.filename)}
>Restore</Button
>{$t('restore')}</Button
>
<IconButton
shape="round"
variant="ghost"
@@ -207,7 +234,7 @@
class="shrink-0"
disabled={deleting.has(backup.filename)}
onclick={(event: Event) => handleOpen(event, { position: 'top-right' }, backup.filename)}
aria-label="Open menu"
aria-label={$t('open')}
/>
</HStack>
</CardBody>

View File

@@ -4,6 +4,7 @@
import { Button, Card, CardBody, Heading, HStack, Icon, Scrollable, Stack, Text } from '@immich/ui';
import { mdiAlert, mdiArrowLeft, mdiArrowRight, mdiCheck, mdiClose, mdiRefresh } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
end?: () => void;
@@ -19,22 +20,11 @@
}
onMount(reload);
const i18nMap = {
'encoded-video': 'Encoded Video',
library: 'Library',
upload: 'Upload',
profile: 'Profile',
thumbs: 'Thumbs',
backups: 'Backups',
};
</script>
{#if stage === 0}
<Heading size="large" color="primary" tag="h1">Restore Your Library</Heading>
<Text
>Before restoring a database backup, you must ensure your library has been restored or is otherwise already present.</Text
>
<Heading size="large" color="primary" tag="h1">{$t('maintenance_restore_library')}</Heading>
<Text>{$t('maintenance_restore_library_description')}</Text>
<Card>
<CardBody>
<Stack>
@@ -46,12 +36,10 @@
color={`rgb(var(--immich-ui-${writable ? 'success' : 'danger'}))`}
/>
<Text
>{i18nMap[folder as keyof typeof i18nMap]} ({writable
? 'readable and writable'
: readable
? 'not writable'
: 'not readable'})</Text
>
>{folder} ({$t(
`maintenance_restore_library_folder_${writable ? 'pass' : readable ? 'write_fail' : 'read_fail'}`,
)})
</Text>
</HStack>
{/each}
{#each integrity.storage as { folder, files } (folder)}
@@ -65,47 +53,57 @@
<Stack gap={0} class="items-start">
<Text>
{#if files}
{i18nMap[folder as keyof typeof i18nMap]} has {files} folder(s)
{$t('maintenance_restore_library_folder_has_files', {
values: {
folder,
count: files,
},
})}
{:else}
{i18nMap[folder as keyof typeof i18nMap]} is missing files!
{$t('maintenance_restore_library_folder_no_files', {
values: {
folder,
},
})}
{/if}
</Text>
{#if !files && (folder === 'profile' || folder === 'upload')}
<Text variant="italic">You may be missing files</Text>
<Text variant="italic">{$t('maintenance_restore_library_hint_missing_files')}</Text>
{/if}
{#if !files && (folder === 'encoded-video' || folder === 'thumbs')}
<Text variant="italic">You can regenerate these later in settings</Text>
<Text variant="italic">{$t('maintenance_restore_library_hint_regenerate_later')}</Text>
{/if}
{#if !files && folder === 'library'}
<Text variant="italic">Using storage template? You may be missing files</Text>
<Text variant="italic">{$t('maintenance_restore_library_hint_storage_template_missing_files')}</Text
>
{/if}
</Stack>
</HStack>
{/if}
{/each}
<Button leadingIcon={mdiRefresh} variant="ghost" onclick={reload}>Refresh</Button>
<Button leadingIcon={mdiRefresh} variant="ghost" onclick={reload}>{$t('refresh')}</Button>
{:else}
<HStack>
<Icon icon={mdiRefresh} color="rgb(var(--immich-ui-primary))" />
<Text>Loading integrity checks and heuristics...</Text>
<Text>{$t('maintenance_restore_library_loading')}</Text>
</HStack>
{/if}
</Stack>
</CardBody>
</Card>
<Text>If this looks correct, continue to restoring a backup!</Text>
<Text>{$t('maintenance_restore_library_confirm')}</Text>
<HStack>
<Button onclick={props.end} variant="ghost">Cancel</Button>
<Button onclick={() => stage++} trailingIcon={mdiArrowRight}>Next</Button>
<Button onclick={props.end} variant="ghost">{$t('cancel')}</Button>
<Button onclick={() => stage++} trailingIcon={mdiArrowRight}>{$t('next')}</Button>
</HStack>
{:else}
<Heading size="large" color="primary" tag="h1">Restore From Backup</Heading>
<Heading size="large" color="primary" tag="h1">{$t('maintenance_restore_from_backup')}</Heading>
<Scrollable class="max-h-80">
<MaintenanceBackupsList />
</Scrollable>
<HStack>
<Button onclick={props.end} variant="ghost">Cancel</Button>
<Button onclick={() => stage--} variant="ghost" leadingIcon={mdiArrowLeft}>Back</Button>
<Button onclick={props.end} variant="ghost">{$t('cancel')}</Button>
<Button onclick={() => stage--} variant="ghost" leadingIcon={mdiArrowLeft}>{$t('back')}</Button>
</HStack>
{/if}

View File

@@ -28,7 +28,7 @@
<span class="px-2 font-semibold">{$t('getting_started')}</span>
</Button>
<Button size="medium" shape="round" variant="ghost" onclick={switchToMaintenance}>
<span class="px-2 font-semibold">Restore from backup</span>
<span class="px-2 font-semibold">{$t('maintenance_restore_library')}</span>
</Button>
</Stack>
</div>

View File

@@ -43,7 +43,7 @@
>
<div class="flex flex-col place-items-center text-center gap-4">
{#if $status?.action === MaintenanceAction.RestoreDatabase && $status.task}
<Heading size="large" color="primary" tag="h1">Restoring Database</Heading>
<Heading size="large" color="primary" tag="h1">{$t('maintenance_action_restore')}</Heading>
{#if $status.error}
<Scrollable class="max-h-80">
<pre class="text-left"><code>{error}</code></pre>