feat(localization): add model description translations and enhance UI text across multiple languages

This commit is contained in:
Will Miao
2025-08-31 10:12:54 +08:00
parent 6acccbbb94
commit 867ffd1163
15 changed files with 313 additions and 30 deletions

View File

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

View File

@@ -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();
}

View File

@@ -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'));
}
});

View File

@@ -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>

View File

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

View File

@@ -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