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 || '
No model description available
'; + const noDescriptionText = await safeTranslate('modals.model.description.noDescription', {}, 'No model description available'); + descriptionContent.innerHTML = description || `
${noDescriptionText}
`; descriptionContent.dataset.loaded = 'true'; // Set up editing functionality - setupModelDescriptionEditing(filePath); + await setupModelDescriptionEditing(filePath); } catch (error) { console.error('Error loading model description:', error); - descriptionContent.innerHTML = '
Failed to load model description
'; + const failedText = await safeTranslate('modals.model.description.failedToLoad', {}, 'Failed to load model description'); + descriptionContent.innerHTML = `
${failedText}
`; } finally { // Hide loading state descriptionLoading?.classList.add('hidden'); @@ -82,7 +85,7 @@ async function loadModelDescription() { * Set up model description editing functionality * @param {string} filePath - File path */ -export function setupModelDescriptionEditing(filePath) { +export async function setupModelDescriptionEditing(filePath) { const descContent = document.querySelector('.model-description-content'); const descContainer = document.querySelector('.model-description-container'); if (!descContent || !descContainer) return; @@ -92,7 +95,9 @@ export function setupModelDescriptionEditing(filePath) { if (!editBtn) { editBtn = document.createElement('button'); editBtn.className = 'edit-model-description-btn'; - editBtn.title = 'Edit model description'; + // Set title using i18n + const editTitle = await safeTranslate('modals.model.description.editTitle', {}, 'Edit model description'); + editBtn.title = editTitle; editBtn.innerHTML = ''; descContainer.insertBefore(editBtn, descContent); } @@ -149,7 +154,8 @@ export function setupModelDescriptionEditing(filePath) { } if (!newValue) { this.innerHTML = originalValue; - showToast('Description cannot be empty', 'error'); + const emptyErrorText = await safeTranslate('modals.model.description.validation.cannotBeEmpty', {}, 'Description cannot be empty'); + showToast(emptyErrorText, 'error'); exitEditMode(); return; } @@ -157,10 +163,12 @@ export function setupModelDescriptionEditing(filePath) { // Save to backend const { getModelApiClient } = await import('../../api/modelApiFactory.js'); await getModelApiClient().saveModelMetadata(filePath, { modelDescription: newValue }); - showToast('Model description updated', 'success'); + const successText = await safeTranslate('modals.model.description.messages.updated', {}, 'Model description updated'); + showToast(successText, 'success'); } catch (err) { this.innerHTML = originalValue; - showToast('Failed to update model description', 'error'); + const errorText = await safeTranslate('modals.model.description.messages.updateFailed', {}, 'Failed to update model description'); + showToast(errorText, 'error'); } finally { exitEditMode(); } diff --git a/static/js/components/shared/ModelMetadata.js b/static/js/components/shared/ModelMetadata.js index d3e3de76..c3fb0d82 100644 --- a/static/js/components/shared/ModelMetadata.js +++ b/static/js/components/shared/ModelMetadata.js @@ -5,6 +5,7 @@ import { showToast } from '../../utils/uiHelpers.js'; import { BASE_MODELS } from '../../utils/constants.js'; import { getModelApiClient } from '../../api/modelApiFactory.js'; +import { safeTranslate } from '../../utils/i18nHelpers.js'; /** * Set up model name editing functionality @@ -82,7 +83,8 @@ export function setupModelNameEditing(filePath) { sel.removeAllRanges(); sel.addRange(range); - showToast('Model name is limited to 100 characters', 'warning'); + safeTranslate('modelMetadata.validation.nameTooLong', {}, 'Model name is limited to 100 characters') + .then(text => showToast(text, 'warning')); } }); diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index 27a9f573..6018fa1d 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -18,6 +18,7 @@ import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js'; import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js'; import { parsePresets, renderPresetTags } from './PresetTags.js'; import { loadRecipesForLora } from './RecipeTab.js'; +import { safeTranslate } from '../../utils/i18nHelpers.js'; /** * Display the model modal with the given model data @@ -61,24 +62,33 @@ export async function showModelModal(model, modelType) { } // Generate tabs based on model type + const examplesText = await safeTranslate('modals.model.tabs.examples', {}, 'Examples'); + const descriptionText = await safeTranslate('modals.model.tabs.description', {}, 'Model Description'); + const recipesText = await safeTranslate('modals.model.tabs.recipes', {}, 'Recipes'); + const tabsContent = modelType === 'loras' ? - ` - - ` : - ` - `; + ` + + ` : + ` + `; + + const loadingExampleImagesText = await safeTranslate('modals.model.loading.exampleImages', {}, 'Loading example images...'); + const loadingDescriptionText = await safeTranslate('modals.model.loading.description', {}, 'Loading model description...'); + const loadingRecipesText = await safeTranslate('modals.model.loading.recipes', {}, 'Loading recipes...'); + const loadingExamplesText = await safeTranslate('modals.model.loading.examples', {}, 'Loading examples...'); const tabPanesContent = modelType === 'loras' ? `
- Loading example images... + ${loadingExampleImagesText}
- Loading model description... + ${loadingDescriptionText}
@@ -87,19 +97,19 @@ export async function showModelModal(model, modelType) {
- Loading recipes... + ${loadingRecipesText}
` : `
- Loading examples... + ${loadingExamplesText}
- Loading model description... + ${loadingDescriptionText}
diff --git a/static/js/components/shared/ModelTags.js b/static/js/components/shared/ModelTags.js index 3ba523f2..499b5889 100644 --- a/static/js/components/shared/ModelTags.js +++ b/static/js/components/shared/ModelTags.js @@ -4,6 +4,7 @@ */ import { showToast } from '../../utils/uiHelpers.js'; import { getModelApiClient } from '../../api/modelApiFactory.js'; +import { safeTranslate } from '../../utils/i18nHelpers.js'; // Preset tag suggestions const PRESET_TAGS = [ @@ -216,10 +217,10 @@ async function saveTags() { // Exit edit mode editBtn.click(); - showToast('Tags updated successfully', 'success'); + showToast(await safeTranslate('modelTags.messages.updated', {}, 'Tags updated successfully'), 'success'); } catch (error) { console.error('Error saving tags:', error); - showToast('Failed to update tags', 'error'); + showToast(await safeTranslate('modelTags.messages.updateFailed', {}, 'Failed to update tags'), 'error'); } } @@ -361,21 +362,24 @@ function addNewTag(tag) { // Validation: Check length if (tag.length > 30) { - showToast('Tag should not exceed 30 characters', 'error'); + safeTranslate('modelTags.validation.maxLength', {}, 'Tag should not exceed 30 characters') + .then(text => showToast(text, 'error')); return; } // Validation: Check total number const currentTags = tagsContainer.querySelectorAll('.metadata-item'); if (currentTags.length >= 30) { - showToast('Maximum 30 tags allowed', 'error'); + safeTranslate('modelTags.validation.maxCount', {}, 'Maximum 30 tags allowed') + .then(text => showToast(text, 'error')); return; } // Validation: Check for duplicates const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag); if (existingTags.includes(tag)) { - showToast('This tag already exists', 'error'); + safeTranslate('modelTags.validation.duplicate', {}, 'This tag already exists') + .then(text => showToast(text, 'error')); return; } diff --git a/static/js/utils/i18nHelpers.js b/static/js/utils/i18nHelpers.js index 80431ed9..141ef91f 100644 --- a/static/js/utils/i18nHelpers.js +++ b/static/js/utils/i18nHelpers.js @@ -2,6 +2,26 @@ * i18n utility functions for safe translation handling */ +/** + * Synchronous translation function. + * Assumes window.i18n is ready. + * @param {string} key - Translation key + * @param {Object} params - Parameters for interpolation + * @param {string} fallback - Fallback text if translation fails + * @returns {string} Translated text + */ +export function translate(key, params = {}, fallback = null) { + if (!window.i18n) { + console.warn('i18n not available'); + return fallback || key; + } + const translation = window.i18n.t(key, params); + if (translation === key && fallback) { + return fallback; + } + return translation; +} + /** * Safe translation function that waits for i18n to be ready * @param {string} key - Translation key