mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(localization): add model description translations and enhance UI text across multiple languages
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -328,6 +328,20 @@
|
||||
"x": "成人向け",
|
||||
"xxx": "露骨"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"description": {
|
||||
"noDescription": "モデルの説明がありません",
|
||||
"failedToLoad": "モデルの説明の読み込みに失敗しました",
|
||||
"editTitle": "モデルの説明を編集",
|
||||
"validation": {
|
||||
"cannotBeEmpty": "説明を空にすることはできません"
|
||||
},
|
||||
"messages": {
|
||||
"updated": "モデルの説明を更新しました",
|
||||
"updateFailed": "モデルの説明の更新に失敗しました"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -328,6 +328,20 @@
|
||||
"x": "성인 전용",
|
||||
"xxx": "성인 노골적"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"description": {
|
||||
"noDescription": "모델 설명이 없습니다",
|
||||
"failedToLoad": "모델 설명 로드에 실패했습니다",
|
||||
"editTitle": "모델 설명 편집",
|
||||
"validation": {
|
||||
"cannotBeEmpty": "설명은 비어있을 수 없습니다"
|
||||
},
|
||||
"messages": {
|
||||
"updated": "모델 설명이 업데이트되었습니다",
|
||||
"updateFailed": "모델 설명 업데이트에 실패했습니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -328,6 +328,20 @@
|
||||
"x": "Только для взрослых",
|
||||
"xxx": "Откровенное содержание"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"description": {
|
||||
"noDescription": "Описание модели недоступно",
|
||||
"failedToLoad": "Не удалось загрузить описание модели",
|
||||
"editTitle": "Редактировать описание модели",
|
||||
"validation": {
|
||||
"cannotBeEmpty": "Описание не может быть пустым"
|
||||
},
|
||||
"messages": {
|
||||
"updated": "Описание модели обновлено",
|
||||
"updateFailed": "Не удалось обновить описание модели"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -328,6 +328,20 @@
|
||||
"x": "成人級",
|
||||
"xxx": "重口級"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"description": {
|
||||
"noDescription": "無模型描述資訊",
|
||||
"failedToLoad": "載入模型描述失敗",
|
||||
"editTitle": "編輯模型描述",
|
||||
"validation": {
|
||||
"cannotBeEmpty": "描述不能為空"
|
||||
},
|
||||
"messages": {
|
||||
"updated": "模型描述已更新",
|
||||
"updateFailed": "更新模型描述失敗"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || '<div class="no-description">No model description available</div>';
|
||||
const noDescriptionText = await safeTranslate('modals.model.description.noDescription', {}, 'No model description available');
|
||||
descriptionContent.innerHTML = description || `<div class="no-description">${noDescriptionText}</div>`;
|
||||
descriptionContent.dataset.loaded = 'true';
|
||||
|
||||
// Set up editing functionality
|
||||
setupModelDescriptionEditing(filePath);
|
||||
await setupModelDescriptionEditing(filePath);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading model description:', error);
|
||||
descriptionContent.innerHTML = '<div class="no-description">Failed to load model description</div>';
|
||||
const failedText = await safeTranslate('modals.model.description.failedToLoad', {}, 'Failed to load model description');
|
||||
descriptionContent.innerHTML = `<div class="no-description">${failedText}</div>`;
|
||||
} 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 = '<i class="fas fa-pencil-alt"></i>';
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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' ?
|
||||
`<button class="tab-btn active" data-tab="showcase">Examples</button>
|
||||
<button class="tab-btn" data-tab="description">Model Description</button>
|
||||
<button class="tab-btn" data-tab="recipes">Recipes</button>` :
|
||||
`<button class="tab-btn active" data-tab="showcase">Examples</button>
|
||||
<button class="tab-btn" data-tab="description">Model Description</button>`;
|
||||
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
||||
<button class="tab-btn" data-tab="description">${descriptionText}</button>
|
||||
<button class="tab-btn" data-tab="recipes">${recipesText}</button>` :
|
||||
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
||||
<button class="tab-btn" data-tab="description">${descriptionText}</button>`;
|
||||
|
||||
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' ?
|
||||
`<div id="showcase-tab" class="tab-pane active">
|
||||
<div class="example-images-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading example images...
|
||||
<i class="fas fa-spinner fa-spin"></i> ${loadingExampleImagesText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="description-tab" class="tab-pane">
|
||||
<div class="model-description-container">
|
||||
<div class="model-description-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
||||
<i class="fas fa-spinner fa-spin"></i> ${loadingDescriptionText}
|
||||
</div>
|
||||
<div class="model-description-content hidden">
|
||||
</div>
|
||||
@@ -87,19 +97,19 @@ export async function showModelModal(model, modelType) {
|
||||
|
||||
<div id="recipes-tab" class="tab-pane">
|
||||
<div class="recipes-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading recipes...
|
||||
<i class="fas fa-spinner fa-spin"></i> ${loadingRecipesText}
|
||||
</div>
|
||||
</div>` :
|
||||
`<div id="showcase-tab" class="tab-pane active">
|
||||
<div class="recipes-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading examples...
|
||||
<i class="fas fa-spinner fa-spin"></i> ${loadingExamplesText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="description-tab" class="tab-pane">
|
||||
<div class="model-description-container">
|
||||
<div class="model-description-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
||||
<i class="fas fa-spinner fa-spin"></i> ${loadingDescriptionText}
|
||||
</div>
|
||||
<div class="model-description-content hidden">
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user