From 867ffd11633b22a24e09ef2a8f35b5ddd3b7bad3 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 31 Aug 2025 10:12:54 +0800 Subject: [PATCH] feat(localization): add model description translations and enhance UI text across multiple languages --- locales/de.json | 14 ++++ locales/en.json | 67 +++++++++++++++++++ locales/es.json | 14 ++++ locales/fr.json | 14 ++++ locales/ja.json | 14 ++++ locales/ko.json | 14 ++++ locales/ru.json | 14 ++++ locales/zh-CN.json | 67 +++++++++++++++++++ locales/zh-TW.json | 14 ++++ static/js/components/shared/ModelCard.js | 19 ++++-- .../js/components/shared/ModelDescription.js | 24 ++++--- static/js/components/shared/ModelMetadata.js | 4 +- static/js/components/shared/ModelModal.js | 30 ++++++--- static/js/components/shared/ModelTags.js | 14 ++-- static/js/utils/i18nHelpers.js | 20 ++++++ 15 files changed, 313 insertions(+), 30 deletions(-) diff --git a/locales/de.json b/locales/de.json index 1346ad69..a7503fd7 100644 --- a/locales/de.json +++ b/locales/de.json @@ -328,6 +328,20 @@ "x": "Nur Erwachsene", "xxx": "Explizit" } + }, + "model": { + "description": { + "noDescription": "Keine Modellbeschreibung verfügbar", + "failedToLoad": "Fehler beim Laden der Modellbeschreibung", + "editTitle": "Modellbeschreibung bearbeiten", + "validation": { + "cannotBeEmpty": "Beschreibung darf nicht leer sein" + }, + "messages": { + "updated": "Modellbeschreibung aktualisiert", + "updateFailed": "Fehler beim Aktualisieren der Modellbeschreibung" + } + } } }, "errors": { diff --git a/locales/en.json b/locales/en.json index 497875c3..6f2818cd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -73,6 +73,48 @@ "korean": "한국어", "french": "Français", "spanish": "Español" + }, + "modelCard": { + "favorites": { + "added": "Added to favorites", + "removed": "Removed from favorites", + "updateFailed": "Failed to update favorite status" + }, + "sendToWorkflow": { + "checkpointNotImplemented": "Send checkpoint to workflow - feature to be implemented" + }, + "exampleImages": { + "checkError": "Error checking for example images", + "missingHash": "Missing model hash information." + } + }, + "modelTags": { + "messages": { + "updated": "Tags updated successfully", + "updateFailed": "Failed to update tags" + }, + "validation": { + "maxLength": "Tag should not exceed 30 characters", + "maxCount": "Maximum 30 tags allowed", + "duplicate": "This tag already exists" + } + }, + "modelMetadata": { + "validation": { + "nameTooLong": "Model name is limited to 100 characters", + "nameEmpty": "Model name cannot be empty" + }, + "messages": { + "nameUpdated": "Model name updated successfully", + "nameUpdateFailed": "Failed to update model name", + "baseModelUpdated": "Base model updated successfully", + "baseModelUpdateFailed": "Failed to update base model" + } + }, + "recipeTab": { + "noRecipesFound": "No recipes found that use this Lora.", + "loadingRecipes": "Loading recipes...", + "errorLoadingRecipes": "Failed to load recipes. Please try again later." } }, "header": { @@ -443,6 +485,31 @@ "note": "Note: If no modelVersionId is provided, the latest version will be used." }, "confirmAction": "Confirm Re-link" + }, + "model": { + "description": { + "noDescription": "No model description available", + "failedToLoad": "Failed to load model description", + "editTitle": "Edit model description", + "validation": { + "cannotBeEmpty": "Description cannot be empty" + }, + "messages": { + "updated": "Model description updated", + "updateFailed": "Failed to update model description" + } + }, + "tabs": { + "examples": "Examples", + "description": "Model Description", + "recipes": "Recipes" + }, + "loading": { + "exampleImages": "Loading example images...", + "description": "Loading model description...", + "recipes": "Loading recipes...", + "examples": "Loading examples..." + } } }, "errors": { diff --git a/locales/es.json b/locales/es.json index 67b1851c..7e27dfa7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -328,6 +328,20 @@ "x": "Solo adultos", "xxx": "Explícito" } + }, + "model": { + "description": { + "noDescription": "No hay descripción del modelo disponible", + "failedToLoad": "Error al cargar la descripción del modelo", + "editTitle": "Editar descripción del modelo", + "validation": { + "cannotBeEmpty": "La descripción no puede estar vacía" + }, + "messages": { + "updated": "Descripción del modelo actualizada", + "updateFailed": "Error al actualizar la descripción del modelo" + } + } } }, "errors": { diff --git a/locales/fr.json b/locales/fr.json index a72276a6..8fe572b0 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -328,6 +328,20 @@ "x": "Adultes seulement", "xxx": "Explicite" } + }, + "model": { + "description": { + "noDescription": "Aucune description de modèle disponible", + "failedToLoad": "Échec du chargement de la description du modèle", + "editTitle": "Modifier la description du modèle", + "validation": { + "cannotBeEmpty": "La description ne peut pas être vide" + }, + "messages": { + "updated": "Description du modèle mise à jour", + "updateFailed": "Échec de la mise à jour de la description du modèle" + } + } } }, "errors": { diff --git a/locales/ja.json b/locales/ja.json index 58562d97..11626949 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -328,6 +328,20 @@ "x": "成人向け", "xxx": "露骨" } + }, + "model": { + "description": { + "noDescription": "モデルの説明がありません", + "failedToLoad": "モデルの説明の読み込みに失敗しました", + "editTitle": "モデルの説明を編集", + "validation": { + "cannotBeEmpty": "説明を空にすることはできません" + }, + "messages": { + "updated": "モデルの説明を更新しました", + "updateFailed": "モデルの説明の更新に失敗しました" + } + } } }, "errors": { diff --git a/locales/ko.json b/locales/ko.json index b1e26092..aed19d78 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -328,6 +328,20 @@ "x": "성인 전용", "xxx": "성인 노골적" } + }, + "model": { + "description": { + "noDescription": "모델 설명이 없습니다", + "failedToLoad": "모델 설명 로드에 실패했습니다", + "editTitle": "모델 설명 편집", + "validation": { + "cannotBeEmpty": "설명은 비어있을 수 없습니다" + }, + "messages": { + "updated": "모델 설명이 업데이트되었습니다", + "updateFailed": "모델 설명 업데이트에 실패했습니다" + } + } } }, "errors": { diff --git a/locales/ru.json b/locales/ru.json index aba61fee..c756111e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -328,6 +328,20 @@ "x": "Только для взрослых", "xxx": "Откровенное содержание" } + }, + "model": { + "description": { + "noDescription": "Описание модели недоступно", + "failedToLoad": "Не удалось загрузить описание модели", + "editTitle": "Редактировать описание модели", + "validation": { + "cannotBeEmpty": "Описание не может быть пустым" + }, + "messages": { + "updated": "Описание модели обновлено", + "updateFailed": "Не удалось обновить описание модели" + } + } } }, "errors": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 0b9e2e2e..49d4533e 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -73,6 +73,48 @@ "korean": "한국어", "french": "Français", "spanish": "Español" + }, + "modelCard": { + "favorites": { + "added": "已添加到收藏", + "removed": "已从收藏中移除", + "updateFailed": "更新收藏状态失败" + }, + "sendToWorkflow": { + "checkpointNotImplemented": "发送 Checkpoint 到工作流 - 功能待实现" + }, + "exampleImages": { + "checkError": "检查示例图片时出错", + "missingHash": "缺少模型哈希信息。" + } + }, + "modelTags": { + "messages": { + "updated": "标签更新成功", + "updateFailed": "更新标签失败" + }, + "validation": { + "maxLength": "标签长度不能超过30个字符", + "maxCount": "最多允许30个标签", + "duplicate": "该标签已存在" + } + }, + "modelMetadata": { + "validation": { + "nameTooLong": "模型名称最多100个字符", + "nameEmpty": "模型名称不能为空" + }, + "messages": { + "nameUpdated": "模型名称更新成功", + "nameUpdateFailed": "更新模型名称失败", + "baseModelUpdated": "基础模型更新成功", + "baseModelUpdateFailed": "更新基础模型失败" + } + }, + "recipeTab": { + "noRecipesFound": "未找到使用此 LoRA 的配方。", + "loadingRecipes": "正在加载配方...", + "errorLoadingRecipes": "加载配方失败。请稍后重试。" } }, "header": { @@ -443,6 +485,31 @@ "note": "注意:如果未提供 modelVersionId,将使用最新版本。" }, "confirmAction": "确认重新链接" + }, + "model": { + "description": { + "noDescription": "无模型描述信息", + "failedToLoad": "加载模型描述失败", + "editTitle": "编辑模型描述", + "validation": { + "cannotBeEmpty": "描述不能为空" + }, + "messages": { + "updated": "模型描述已更新", + "updateFailed": "更新模型描述失败" + } + }, + "tabs": { + "examples": "示例图片", + "description": "模型描述", + "recipes": "配方" + }, + "loading": { + "exampleImages": "正在加载示例图片...", + "description": "正在加载模型描述...", + "recipes": "正在加载配方...", + "examples": "正在加载示例..." + } } }, "errors": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 83bdb1cf..601c5d76 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -328,6 +328,20 @@ "x": "成人級", "xxx": "重口級" } + }, + "model": { + "description": { + "noDescription": "無模型描述資訊", + "failedToLoad": "載入模型描述失敗", + "editTitle": "編輯模型描述", + "validation": { + "cannotBeEmpty": "描述不能為空" + }, + "messages": { + "updated": "模型描述已更新", + "updateFailed": "更新模型描述失敗" + } + } } }, "errors": { diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index f309be77..da245bcb 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -8,6 +8,7 @@ import { NSFW_LEVELS } from '../../utils/constants.js'; import { MODEL_TYPES } from '../../api/apiConfig.js'; import { getModelApiClient } from '../../api/modelApiFactory.js'; import { showDeleteModal } from '../../utils/modalUtils.js'; +import { safeTranslate } from '../../utils/i18nHelpers.js'; // Add global event delegation handlers export function setupModelCardEventDelegation(modelType) { @@ -142,13 +143,16 @@ async function toggleFavorite(card) { }); if (newFavoriteState) { - showToast('Added to favorites', 'success'); + const addedText = await safeTranslate('modelCard.favorites.added', {}, 'Added to favorites'); + showToast(addedText, 'success'); } else { - showToast('Removed from favorites', 'success'); + const removedText = await safeTranslate('modelCard.favorites.removed', {}, 'Removed from favorites'); + showToast(removedText, 'success'); } } catch (error) { console.error('Failed to update favorite status:', error); - showToast('Failed to update favorite status', 'error'); + const errorText = await safeTranslate('modelCard.favorites.updateFailed', {}, 'Failed to update favorite status'); + showToast(errorText, 'error'); } } @@ -160,7 +164,8 @@ function handleSendToWorkflow(card, replaceMode, modelType) { sendLoraToWorkflow(loraSyntax, replaceMode, 'lora'); } else { // Checkpoint send functionality - to be implemented - showToast('Send checkpoint to workflow - feature to be implemented', 'info'); + safeTranslate('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'Send checkpoint to workflow - feature to be implemented') + .then(text => showToast(text, 'info')); } } @@ -195,7 +200,8 @@ async function handleExampleImagesAccess(card, modelType) { } } catch (error) { console.error('Error checking for example images:', error); - showToast('Error checking for example images', 'error'); + safeTranslate('modelCard.exampleImages.checkError', {}, 'Error checking for example images') + .then(text => showToast(text, 'error')); } } @@ -277,7 +283,8 @@ function showExampleAccessModal(card, modelType) { // Get the model hash const modelHash = card.dataset.sha256; if (!modelHash) { - showToast('Missing model hash information.', 'error'); + safeTranslate('modelCard.exampleImages.missingHash', {}, 'Missing model hash information.') + .then(text => showToast(text, 'error')); return; } diff --git a/static/js/components/shared/ModelDescription.js b/static/js/components/shared/ModelDescription.js index 4699090e..fab3f705 100644 --- a/static/js/components/shared/ModelDescription.js +++ b/static/js/components/shared/ModelDescription.js @@ -1,4 +1,5 @@ import { showToast } from '../../utils/uiHelpers.js'; +import { safeTranslate } from '../../utils/i18nHelpers.js'; /** * ModelDescription.js @@ -62,15 +63,17 @@ async function loadModelDescription() { const description = await getModelApiClient().fetchModelDescription(filePath); // Update content - descriptionContent.innerHTML = description || '