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 b3c0a1c593..98d04f439f 100644
--- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
+++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
@@ -61,12 +61,12 @@
[JobName.ThumbnailGeneration]: {
icon: mdiFileJpgBox,
title: getJobName(JobName.ThumbnailGeneration),
- subtitle: $t('thumbnail_generation_job_description'),
+ subtitle: $t('admin.thumbnail_generation_job_description'),
},
[JobName.MetadataExtraction]: {
icon: mdiTable,
title: getJobName(JobName.MetadataExtraction),
- subtitle: $t('metadata_extraction_job_description'),
+ subtitle: $t('admin.metadata_extraction_job_description'),
},
[JobName.Library]: {
icon: mdiLibraryShelves,
@@ -78,7 +78,7 @@
[JobName.Sidecar]: {
title: getJobName(JobName.Sidecar),
icon: mdiFileXmlBox,
- subtitle: $t('sidecar_job_description'),
+ subtitle: $t('admin.sidecar_job_description'),
allText: $t('sync').toUpperCase(),
missingText: $t('discover').toUpperCase(),
disabled: !$featureFlags.sidecar,
@@ -86,13 +86,13 @@
[JobName.SmartSearch]: {
icon: mdiImageSearch,
title: getJobName(JobName.SmartSearch),
- subtitle: $t('smart_search_job_description'),
+ subtitle: $t('admin.smart_search_job_description'),
disabled: !$featureFlags.smartSearch,
},
[JobName.DuplicateDetection]: {
icon: mdiContentDuplicate,
title: getJobName(JobName.DuplicateDetection),
- subtitle: $t('duplicate_detection_job_description'),
+ subtitle: $t('admin.duplicate_detection_job_description'),
disabled: !$featureFlags.duplicateDetection,
},
[JobName.FaceDetection]: {
@@ -114,7 +114,7 @@
[JobName.VideoConversion]: {
icon: mdiVideo,
title: getJobName(JobName.VideoConversion),
- subtitle: $t('video_conversion_job_description'),
+ subtitle: $t('admin.video_conversion_job_description'),
},
[JobName.StorageTemplateMigration]: {
icon: mdiFolderMove,
@@ -125,7 +125,7 @@
[JobName.Migration]: {
icon: mdiFolderMove,
title: getJobName(JobName.Migration),
- subtitle: $t('migration_job_description'),
+ subtitle: $t('admin.migration_job_description'),
allowForceCommand: false,
},
};
diff --git a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte
index a33ad1b7ff..4de943d64a 100644
--- a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte
+++ b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte
@@ -5,6 +5,6 @@
Apply the current
{$t('storage_template_settings')}{$t('admin.storage_template_settings')}
to previously uploaded assets
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 ac2d354523..7094d147ea 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('disabled'),
+ text: $t('admin.disabled'),
},
]}
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
diff --git a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte
index f2f8fbada0..751440baf4 100644
--- a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte
@@ -100,7 +100,7 @@
href="https://crontab.guru"
class="underline"
target="_blank"
- rel="noreferrer">{$t('crontab_guru')}{$t('admin.crontab_guru')}
diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte
index 14a2a69767..9c155b68e4 100644
--- a/web/src/lib/components/album-page/albums-list.svelte
+++ b/web/src/lib/components/album-page/albums-list.svelte
@@ -315,7 +315,7 @@
await handleDeleteAlbum(albumToDelete);
} catch {
notificationController.show({
- message: $t('errors.errors.unable_to_delete_album'),
+ message: $t('errors.unable_to_delete_album'),
type: NotificationType.Error,
});
} finally {
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte
index 23f32459bb..a2af6c5c1d 100644
--- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte
@@ -25,7 +25,7 @@
- {$t('storage_template_settings').toUpperCase()}
+ {$t('admin.storage_template_settings').toUpperCase()}
diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte
index f2e835aecf..465c695a48 100644
--- a/web/src/lib/components/photos-page/actions/stack-action.svelte
+++ b/web/src/lib/components/photos-page/actions/stack-action.svelte
@@ -40,7 +40,7 @@
{#if unstack}
-
+
{:else}
{/if}
diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/components/slideshow-settings.svelte
index 5998f4e962..e2bf6a4b2c 100644
--- a/web/src/lib/components/slideshow-settings.svelte
+++ b/web/src/lib/components/slideshow-settings.svelte
@@ -68,7 +68,7 @@
diff --git a/web/src/lib/i18n.spec.ts b/web/src/lib/i18n.spec.ts
new file mode 100644
index 0000000000..6bfcc32b43
--- /dev/null
+++ b/web/src/lib/i18n.spec.ts
@@ -0,0 +1,36 @@
+import messages from '$lib/i18n/en.json';
+import { exec as execCallback } from 'node:child_process';
+import { promisify } from 'node:util';
+import { init } from 'svelte-i18n';
+
+type Messages = { [key: string]: string | Messages };
+
+const exec = promisify(execCallback);
+
+function setEmptyMessages(messages: Messages) {
+ const copy = { ...messages };
+
+ for (const key in copy) {
+ const message = copy[key];
+ if (typeof message === 'string') {
+ copy[key] = '';
+ } else if (typeof message === 'object') {
+ setEmptyMessages(message);
+ }
+ }
+
+ return copy;
+}
+
+describe('i18n', () => {
+ beforeEach(() => init({ fallbackLocale: 'dev' }));
+
+ test('no missing messages', async () => {
+ const { stdout } = await exec('npx svelte-i18n extract -c svelte.config.js "src/**/*"');
+ const extractedMessages: Messages = JSON.parse(stdout);
+ const existingMessages = setEmptyMessages(messages);
+
+ // Only translations directly using the store seem to get extracted
+ expect({ ...extractedMessages, ...existingMessages }).toEqual(existingMessages);
+ });
+});
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json
index 1ce471beca..924aaf12a8 100644
--- a/web/src/lib/i18n/en.json
+++ b/web/src/lib/i18n/en.json
@@ -473,7 +473,7 @@
"info": "Info",
"interval": {
"day_at_onepm": "Every day at 1pm",
- "hours": "Every {hours, plural, one {hour} other {{hours, number} hours}",
+ "hours": "Every {hours, plural, one {hour} other {{hours, number} hours}}",
"night_at_midnight": "Every night at midnight",
"night_at_twoam": "Every night at 2am"
},
@@ -552,6 +552,7 @@
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
"not_in_any_album": "Not in any album",
"notes": "Notes",
+ "oauth": "OAuth",
"offline": "Offline",
"ok": "Ok",
"oldest_first": "Oldest first",
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 5c19f5f59a..929a80f249 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -364,7 +364,7 @@
await deleteAlbum({ id: album.id });
await goto(backUrl);
} catch (error) {
- handleError(error, $t('unable_to_delete_album'));
+ handleError(error, $t('errors.unable_to_delete_album'));
} finally {
viewMode = ViewMode.VIEW;
}
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index 685426a1b0..553726afca 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -209,7 +209,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_save_name'));
+ handleError(error, $t('errors.unable_to_save_name'));
}
if (personToBeMergedIn.name !== personName && edittingPerson.id === personToBeMergedIn.id) {
/*
@@ -235,7 +235,7 @@
// trigger reactivity
people = people;
} catch (error) {
- handleError(error, $t('unable_to_save_name'));
+ handleError(error, $t('errors.unable_to_save_name'));
}
}
};
@@ -279,7 +279,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_hide_person'));
+ handleError(error, $t('errors.unable_to_hide_person'));
}
};
@@ -350,7 +350,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_save_name'));
+ handleError(error, $t('errors.unable_to_save_name'));
}
};
@@ -377,7 +377,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_save_name'));
+ handleError(error, $t('errors.unable_to_save_name'));
}
};
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 7a3f5fe634..9392f1b60a 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -189,7 +189,7 @@
await goto(previousRoute, { replaceState: true });
} catch (error) {
- handleError(error, $t('unable_to_hide_person'));
+ handleError(error, $t('errors.unable_to_hide_person'));
}
};
@@ -236,7 +236,7 @@
}
await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true });
} catch (error) {
- handleError(error, $t('unable_to_save_name'));
+ handleError(error, $t('errors.unable_to_save_name'));
}
};
@@ -262,7 +262,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_save_name'));
+ handleError(error, $t('errors.unable_to_save_name'));
}
};
diff --git a/web/src/routes/(user)/utilities/[duplicates]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/[duplicates]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index f29903c7c8..45d50a8e00 100644
--- a/web/src/routes/(user)/utilities/[duplicates]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/utilities/[duplicates]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -40,7 +40,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_resolve_duplicate'));
+ handleError(error, $t('errors.unable_to_resolve_duplicate'));
}
};
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
index 4c4e4ff806..ae4ee75c9e 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -125,7 +125,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_create_library'));
+ handleError(error, $t('errors.unable_to_create_library'));
} finally {
toCreateLibrary = false;
await readLibraryList();
@@ -143,7 +143,7 @@
closeAll();
await readLibraryList();
} catch (error) {
- handleError(error, $t('unable_to_update_library'));
+ handleError(error, $t('errors.unable_to_update_library'));
}
};
@@ -163,7 +163,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_remove_library'));
+ handleError(error, $t('errors.unable_to_remove_library'));
} finally {
confirmDeleteLibrary = null;
deletedLibrary = null;
@@ -181,7 +181,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_scan_libraries'));
+ handleError(error, $t('errors.unable_to_scan_libraries'));
}
};
@@ -193,7 +193,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_scan_library'));
+ handleError(error, $t('errors.unable_to_scan_library'));
}
};
@@ -205,7 +205,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_scan_library'));
+ handleError(error, $t('errors.unable_to_scan_library'));
}
};
@@ -217,7 +217,7 @@
type: NotificationType.Info,
});
} catch (error) {
- handleError(error, $t('unable_to_scan_library'));
+ handleError(error, $t('errors.unable_to_scan_library'));
}
};
diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte
index 2b5a2a38f1..e60d521cdd 100644
--- a/web/src/routes/admin/repair/+page.svelte
+++ b/web/src/routes/admin/repair/+page.svelte
@@ -85,7 +85,7 @@
matches = [];
} catch (error) {
- handleError(error, $t('unable_to_repair_items'));
+ handleError(error, $t('errors.unable_to_repair_items'));
} finally {
repairing = false;
}
@@ -110,7 +110,7 @@
notificationController.show({ message: $t('refreshed'), type: NotificationType.Info });
} catch (error) {
- handleError(error, $t('unable_to_load_items'));
+ handleError(error, $t('errors.unable_to_load_items'));
}
};
@@ -121,7 +121,7 @@
notificationController.show({ message: `Matched 1 item`, type: NotificationType.Info });
}
} catch (error) {
- handleError(error, $t('unable_to_check_item'));
+ handleError(error, $t('errors.unable_to_check_item'));
}
};
@@ -137,7 +137,7 @@
count += await loadAndMatch(filenames.slice(index, index + chunkSize));
}
} catch (error) {
- handleError(error, $t('unable_to_check_items'));
+ handleError(error, $t('errors.unable_to_check_items'));
} finally {
checking = false;
}