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; }