diff --git a/locales/de.json b/locales/de.json index d69d4a26..fddefb9b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -443,6 +443,7 @@ "label": "Bereits heruntergeladene Modellversionen überspringen", "help": "Wenn aktiviert, überspringt LoRA Manager den Download einer Modellversion, wenn der Download-Verlaufsdienst diese spezifische Version als bereits heruntergeladen erfasst hat. Gilt für alle Download-Abläufe." }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Nach Modell gruppieren", "groupByModelHelp": "Wenn aktiviert, wird nur die neueste Version jedes Civitai-Modells als einzelne Karte angezeigt. Ältere Versionen werden ausgeblendet.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "Modell löschen", - "message": "Sind Sie sicher, dass Sie dieses Modell und alle zugehörigen Dateien löschen möchten?" + "message": "Sind Sie sicher, dass Sie dieses Modell und alle zugehörigen Dateien löschen möchten?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "Modell ausschließen", @@ -2113,6 +2117,14 @@ "updateFailed": "Fehler beim Aktualisieren der Trigger Words", "copyFailed": "Kopieren fehlgeschlagen" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "Fehler beim Laden der Elemente", "loadMoreFailed": "Fehler beim Laden weiterer Elemente", diff --git a/locales/en.json b/locales/en.json index 9f554140..7cbbc15e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -443,6 +443,7 @@ "label": "Skip previously downloaded model versions", "help": "When enabled, versions downloaded before will be skipped." }, + "deleteUndoEnabled": "Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Group by Model", "groupByModelHelp": "When enabled, only the latest version of each Civitai model is shown as a single card. Older versions are hidden.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "Frees {size}", "title": "Delete Model", - "message": "Are you sure you want to delete this model and all associated files?" + "message": "Are you sure you want to delete this model and all associated files?", + "permanentWarning": "This will permanently delete the file from disk.", + "recoverableWarning": "This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "Exclude Model", @@ -2113,6 +2117,14 @@ "updateFailed": "Failed to update trigger words", "copyFailed": "Copy failed" }, + "undo": { + "action": "Undo", + "deleted": "Deleted {name}", + "deletedBulk": "Deleted {count} item(s)", + "expired": "Undo window expired. The item was permanently deleted.", + "failed": "Undo failed: {error}", + "restored": "Item restored" + }, "virtual": { "loadFailed": "Failed to load items", "loadMoreFailed": "Failed to load more items", diff --git a/locales/es.json b/locales/es.json index 39159d84..5415617e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -443,6 +443,7 @@ "label": "Omitir versiones de modelos previamente descargadas", "help": "Cuando está habilitado, LoRA Manager omitirá la descarga de una versión de modelo si el servicio de historial de descargas registra esa versión exacta como ya descargada. Aplica a todos los flujos de descarga." }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Agrupar por modelo", "groupByModelHelp": "Cuando está activado, solo se muestra la versión más reciente de cada modelo de Civitai como una tarjeta única. Las versiones anteriores están ocultas.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "Eliminar modelo", - "message": "¿Estás seguro de que quieres eliminar este modelo y todos los archivos asociados?" + "message": "¿Estás seguro de que quieres eliminar este modelo y todos los archivos asociados?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "Excluir modelo", @@ -2113,6 +2117,14 @@ "updateFailed": "Error al actualizar palabras clave", "copyFailed": "Error al copiar" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "Error al cargar elementos", "loadMoreFailed": "Error al cargar más elementos", diff --git a/locales/fr.json b/locales/fr.json index 7be374b2..200a84bb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -443,6 +443,7 @@ "label": "Ignorer les versions de modèles précédemment téléchargées", "help": "Lorsque activé, LoRA Manager ignorera le téléchargement d'une version de modèle si le service d'historique des téléchargements enregistre cette version exacte comme déjà téléchargée. S'applique à tous les flux de téléchargement." }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Grouper par modèle", "groupByModelHelp": "Lorsque activé, seule la version la plus récente de chaque modèle Civitai s'affiche sous forme de carte unique. Les versions plus anciennes sont masquées.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "Supprimer le modèle", - "message": "Êtes-vous sûr de vouloir supprimer ce modèle et tous les fichiers associés ?" + "message": "Êtes-vous sûr de vouloir supprimer ce modèle et tous les fichiers associés ?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "Exclure le modèle", @@ -2113,6 +2117,14 @@ "updateFailed": "Échec de la mise à jour des mots-clés", "copyFailed": "Échec de la copie" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "Échec du chargement des éléments", "loadMoreFailed": "Échec du chargement de plus d'éléments", diff --git a/locales/he.json b/locales/he.json index e292c775..64a8cf2a 100644 --- a/locales/he.json +++ b/locales/he.json @@ -443,6 +443,7 @@ "label": "דלג על גרסאות מודלים שהורדו בעבר", "help": "כאשר מופעל, LoRA Manager ידלג על הורדת גרסת מודל אם שירות היסטוריית ההורדות רושם את הגרסה המדויקת הזו ככבר שהורדה. חל על כל תהליכי ההורדה." }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "קיבוץ לפי דגם", "groupByModelHelp": "כאשר מופעל, רק הגרסה העדכנית ביותר של כל דגם Civitai מוצגת ככרטיס בודד. גרסאות ישנות יותר מוסתרות.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "מחק מודל", - "message": "האם אתה בטוח שברצונך למחוק מודל זה וכל הקבצים הנלווים?" + "message": "האם אתה בטוח שברצונך למחוק מודל זה וכל הקבצים הנלווים?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "החרג מודל", @@ -2113,6 +2117,14 @@ "updateFailed": "עדכון מילות הטריגר נכשל", "copyFailed": "ההעתקה נכשלה" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "טעינת הפריטים נכשלה", "loadMoreFailed": "טעינת פריטים נוספים נכשלה", diff --git a/locales/ja.json b/locales/ja.json index 7fbf6250..36be7466 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -443,6 +443,7 @@ "label": "以前にダウンロードしたモデルバージョンをスキップ", "help": "有効にすると、ダウンロード履歴サービスがそのバージョンが既にダウンロード済みと記録している場合、LoRA Managerはそのモデルバージョンのダウンロードをスキップします。すべてのダウンロードフローに適用されます。" }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "モデルでグループ化", "groupByModelHelp": "有効にすると、各Civitaiモデルの最新バージョンのみが1枚のカードとして表示され、古いバージョンは非表示になります。", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "モデルを削除", - "message": "このモデルと関連するすべてのファイルを削除してもよろしいですか?" + "message": "このモデルと関連するすべてのファイルを削除してもよろしいですか?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "モデルを除外", @@ -2113,6 +2117,14 @@ "updateFailed": "トリガーワードの更新に失敗しました", "copyFailed": "コピーに失敗しました" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "アイテムの読み込みに失敗しました", "loadMoreFailed": "追加アイテムの読み込みに失敗しました", diff --git a/locales/ko.json b/locales/ko.json index 8eaf4173..b45d100b 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -443,6 +443,7 @@ "label": "이전에 다운로드한 모델 버전 건너뛰기", "help": "활성화하면 다운로드 기록 서비스가 해당 버전이 이미 다운로드되었음을 기록한 경우 LoRA Manager는 해당 모델 버전 다운로드를 건너뜁니다. 모든 다운로드 플로우에 적용됩니다." }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "모델별 그룹화", "groupByModelHelp": "활성화하면 각 Civitai 모델의 최신 버전만 단일 카드로 표시되며, 이전 버전은 숨겨집니다.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "모델 삭제", - "message": "이 모델과 모든 관련 파일을 삭제하시겠습니까?" + "message": "이 모델과 모든 관련 파일을 삭제하시겠습니까?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "모델 제외", @@ -2113,6 +2117,14 @@ "updateFailed": "트리거 단어 업데이트에 실패했습니다", "copyFailed": "복사 실패" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "항목 로딩 실패", "loadMoreFailed": "더 많은 항목 로딩 실패", diff --git a/locales/ru.json b/locales/ru.json index 4657ae7d..8f15fab8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -443,6 +443,7 @@ "label": "Пропускать ранее загруженные версии моделей", "help": "Если включено, LoRA Manager будет пропускать загрузку версии модели, если сервис истории загрузок записал, что эта конкретная версия уже загружена. Применяется ко всем потокам загрузки." }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "Группировать по модели", "groupByModelHelp": "При включении отображается только последняя версия каждой модели Civitai в виде одной карточки. Старые версии скрыты.", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "Удалить модель", - "message": "Вы уверены, что хотите удалить эту модель и все связанные файлы?" + "message": "Вы уверены, что хотите удалить эту модель и все связанные файлы?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "Исключить модель", @@ -2113,6 +2117,14 @@ "updateFailed": "Не удалось обновить триггерные слова", "copyFailed": "Копирование не удалось" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "Не удалось загрузить элементы", "loadMoreFailed": "Не удалось загрузить больше элементов", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 4fe7fccb..656f071c 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -443,6 +443,7 @@ "label": "跳过已下载的模型版本", "help": "启用后,如果下载历史服务记录显示该版本已下载,LoRA Manager 将跳过下载该模型版本。适用于所有下载流程。" }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "按模型分组", "groupByModelHelp": "开启后,每个 Civitai 模型仅显示最新版本的单张卡片,旧版本将被隐藏。", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "删除模型", - "message": "你确定要删除此模型及所有相关文件吗?" + "message": "你确定要删除此模型及所有相关文件吗?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "排除模型", @@ -2113,6 +2117,14 @@ "updateFailed": "触发词更新失败", "copyFailed": "复制失败" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "加载项目失败", "loadMoreFailed": "加载更多项目失败", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index e1d42d59..f128073f 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -443,6 +443,7 @@ "label": "跳過已下載的模型版本", "help": "啟用後,如果下載歷史服務記錄顯示該版本已下載,LoRA Manager 將跳過下載該模型版本。適用於所有下載流程。" }, + "deleteUndoEnabled": "[TODO: Translate] Keep deleted items recoverable for 30 seconds (undo)", "layoutSettings": { "groupByModel": "按模型分組", "groupByModelHelp": "啟用後,每個 Civitai 模型僅顯示最新版本的單張卡片,舊版本將被隱藏。", @@ -1277,8 +1278,11 @@ } }, "deleteModel": { + "freesSpace": "[TODO: Translate] Frees {size}", "title": "刪除模型", - "message": "您確定要刪除此模型及所有相關檔案嗎?" + "message": "您確定要刪除此模型及所有相關檔案嗎?", + "permanentWarning": "[TODO: Translate] This will permanently delete the file from disk.", + "recoverableWarning": "[TODO: Translate] This will permanently delete the file after 30 seconds unless you undo." }, "excludeModel": { "title": "排除模型", @@ -2113,6 +2117,14 @@ "updateFailed": "更新觸發詞失敗", "copyFailed": "複製失敗" }, + "undo": { + "action": "[TODO: Translate] Undo", + "deleted": "[TODO: Translate] Deleted {name}", + "deletedBulk": "[TODO: Translate] Deleted {count} item(s)", + "expired": "[TODO: Translate] Undo window expired. The item was permanently deleted.", + "failed": "[TODO: Translate] Undo failed: {error}", + "restored": "[TODO: Translate] Item restored" + }, "virtual": { "loadFailed": "載入項目失敗", "loadMoreFailed": "載入更多項目失敗", diff --git a/tests/frontend/utils/i18nHelpers.test.js b/tests/frontend/utils/i18nHelpers.test.js new file mode 100644 index 00000000..d208aa78 --- /dev/null +++ b/tests/frontend/utils/i18nHelpers.test.js @@ -0,0 +1,96 @@ +import { describe, it, afterEach, expect, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +import { translate } from '../../../static/js/utils/i18nHelpers.js'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); + +/** + * Real en.json translations backed by a minimal `window.i18n` stub that mirrors + * the nested lookup + {param} interpolation of static/js/i18n/index.js, so the + * assertions exercise actual key resolution rather than key presence. + */ +const loadEnTranslations = () => + JSON.parse(readFileSync(resolve(repoRoot, 'locales/en.json'), 'utf8')); + +const installWindowI18n = () => { + const translations = loadEnTranslations(); + + const interpolate = (str, params) => + str.replace(/\{\{?(\w+)\}?\}/g, (match, key) => + params[key] !== undefined ? params[key] : match, + ); + + window.i18n = { + t: (key, params = {}) => { + const keys = key.split('.'); + let value = translations; + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k]; + } else { + console.warn(`Translation key not found: ${key}`); + return key; + } + } + if (typeof value !== 'string') { + console.warn(`Translation key is not a string: ${key}`); + return key; + } + return interpolate(value, params); + }, + getCurrentLocale: () => 'en', + }; +}; + +afterEach(() => { + delete window.i18n; +}); + +describe('translate() with real en.json locale', () => { + it('resolves undo toast keys with interpolation', () => { + installWindowI18n(); + + expect(translate('toast.undo.deleted', { name: 'x' })).toBe('Deleted x'); + expect(translate('toast.undo.deletedBulk', { count: 3 })).toBe('Deleted 3 item(s)'); + expect(translate('toast.undo.action')).toBe('Undo'); + expect(translate('toast.undo.restored')).toBe('Item restored'); + expect(translate('toast.undo.expired')).toBe( + 'Undo window expired. The item was permanently deleted.', + ); + expect(translate('toast.undo.failed', { error: 'boom' })).toBe('Undo failed: boom'); + }); + + it('resolves delete-model modal keys with interpolation', () => { + installWindowI18n(); + + expect(translate('modals.deleteModel.permanentWarning')).toBe( + 'This will permanently delete the file from disk.', + ); + expect(translate('modals.deleteModel.recoverableWarning')).toBe( + 'This will permanently delete the file after 30 seconds unless you undo.', + ); + expect(translate('modals.deleteModel.freesSpace', { size: '1.2 MB' })).toBe( + 'Frees 1.2 MB', + ); + }); + + it('resolves the delete-undo settings label', () => { + installWindowI18n(); + + expect(translate('settings.deleteUndoEnabled')).toBe( + 'Keep deleted items recoverable for 30 seconds (undo)', + ); + }); + + it('returns the raw key when no translation exists (fallback contract)', () => { + installWindowI18n(); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + expect(translate('toast.undo.nonExistentKey')).toBe('toast.undo.nonExistentKey'); + + warnSpy.mockRestore(); + }); +});