Refactor localization handling and improve i18n support across the application

- Replaced `safeTranslate` with `translate` in various components for consistent translation handling.
- Updated Chinese (Simplified and Traditional) localization files to include new keys and improved translations for model card actions, metadata, and usage tips.
- Enhanced the ModelCard, ModelDescription, ModelMetadata, ModelModal, and ModelTags components to utilize the new translation functions.
- Improved user feedback messages for actions like copying to clipboard, saving notes, and updating tags with localized strings.
- Ensured all UI elements reflect the correct translations based on the user's language preference.
This commit is contained in:
Will Miao
2025-08-31 11:19:06 +08:00
parent 75f3764e6c
commit 59010ca431
16 changed files with 1029 additions and 208 deletions

View File

@@ -22,29 +22,6 @@ export function translate(key, params = {}, fallback = null) {
return translation;
}
/**
* Safe translation function. Assumes i18n is already 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 safeTranslate(key, params = {}, fallback = null) {
if (!window.i18n) {
console.warn('i18n not available');
return fallback || key;
}
const translation = window.i18n.t(key, params);
// If translation returned the key (meaning not found), use fallback
if (translation === key && fallback) {
return fallback;
}
return translation;
}
/**
* Update element text with translation
* @param {HTMLElement|string} element - Element or selector
@@ -56,7 +33,7 @@ export function updateElementText(element, key, params = {}, fallback = null) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const text = safeTranslate(key, params, fallback);
const text = translate(key, params, fallback);
el.textContent = text;
}
@@ -72,7 +49,7 @@ export function updateElementAttribute(element, attribute, key, params = {}, fal
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const text = safeTranslate(key, params, fallback);
const text = translate(key, params, fallback);
el.setAttribute(attribute, text);
}

View File

@@ -1,14 +1,22 @@
import { translate } from './i18nHelpers.js';
import { state, getCurrentPageState } from '../state/index.js';
import { getStorageItem, setStorageItem } from './storageHelpers.js';
import { NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
/**
* Utility function to copy text to clipboard with fallback for older browsers
* @param {string} text - The text to copy to clipboard
* @param {string} successMessage - Optional success message to show in toast
* @returns {Promise<boolean>} - Promise that resolves to true if copy was successful
/**
* Utility function to copy text to clipboard with fallback for older browsers
* @param {string} text - The text to copy to clipboard
* @param {string} successMessage - Optional success message to show in toast
* @returns {Promise<boolean>} - Promise that resolves to true if copy was successful
*/
export async function copyToClipboard(text, successMessage = 'Copied to clipboard') {
export async function copyToClipboard(text, successMessage = null) {
const defaultSuccessMessage = successMessage || translate('uiHelpers.clipboard.copied', {}, 'Copied to clipboard');
try {
// Modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
@@ -25,13 +33,14 @@ export async function copyToClipboard(text, successMessage = 'Copied to clipboar
document.body.removeChild(textarea);
}
if (successMessage) {
showToast(successMessage, 'success');
if (defaultSuccessMessage) {
showToast(defaultSuccessMessage, 'success');
}
return true;
} catch (err) {
console.error('Copy failed:', err);
showToast('Copy failed', 'error');
const errorMessage = translate('uiHelpers.clipboard.copyFailed', {}, 'Copy failed');
showToast(errorMessage, 'error');
return false;
}
}
@@ -294,7 +303,8 @@ export function copyLoraSyntax(card) {
const includeTriggerWords = state.global.settings.includeTriggerWords;
if (!includeTriggerWords) {
copyToClipboard(baseSyntax, "LoRA syntax copied to clipboard");
const message = translate('uiHelpers.lora.syntaxCopied', {}, 'LoRA syntax copied to clipboard');
copyToClipboard(baseSyntax, message);
return;
}
@@ -307,10 +317,8 @@ export function copyLoraSyntax(card) {
!Array.isArray(trainedWords) ||
trainedWords.length === 0
) {
copyToClipboard(
baseSyntax,
"LoRA syntax copied to clipboard (no trigger words found)"
);
const message = translate('uiHelpers.lora.syntaxCopiedNoTriggerWords', {}, 'LoRA syntax copied to clipboard (no trigger words found)');
copyToClipboard(baseSyntax, message);
return;
}
@@ -325,10 +333,8 @@ export function copyLoraSyntax(card) {
if (triggers.length > 0) {
finalSyntax = `${baseSyntax}, ${triggers.join(", ")}`;
}
copyToClipboard(
finalSyntax,
"LoRA syntax with trigger words copied to clipboard"
);
const message = translate('uiHelpers.lora.syntaxCopiedWithTriggerWords', {}, 'LoRA syntax with trigger words copied to clipboard');
copyToClipboard(finalSyntax, message);
} else {
// Multiple groups: format with separators
const groups = trainedWords
@@ -348,10 +354,8 @@ export function copyLoraSyntax(card) {
finalSyntax += `\n${"-".repeat(17)}\n${groups[i]}`;
}
}
copyToClipboard(
finalSyntax,
"LoRA syntax with trigger word groups copied to clipboard"
);
const message = translate('uiHelpers.lora.syntaxCopiedWithTriggerWordGroups', {}, 'LoRA syntax with trigger word groups copied to clipboard');
copyToClipboard(finalSyntax, message);
}
}
@@ -384,7 +388,8 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
// Success case - check node count
if (registryData.data.node_count === 0) {
// No nodes found - show warning
showToast('No supported target nodes found in workflow', 'warning');
const message = translate('uiHelpers.workflow.noSupportedNodes', {}, 'No supported target nodes found in workflow');
showToast(message, 'warning');
return false;
} else if (registryData.data.node_count > 1) {
// Multiple nodes - show selector
@@ -397,7 +402,8 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
}
} catch (error) {
console.error('Failed to get registry:', error);
showToast('Failed to communicate with ComfyUI', 'error');
const message = translate('uiHelpers.workflow.communicationFailed', {}, 'Failed to communicate with ComfyUI');
showToast(message, 'error');
return false;
}
}
@@ -429,18 +435,31 @@ async function sendToSpecificNode(nodeIds, loraSyntax, replaceMode, syntaxType)
if (result.success) {
// Use different toast messages based on syntax type
if (syntaxType === 'recipe') {
showToast(`Recipe ${replaceMode ? 'replaced' : 'added'} to workflow`, 'success');
const message = replaceMode ?
translate('uiHelpers.workflow.recipeReplaced', {}, 'Recipe replaced in workflow') :
translate('uiHelpers.workflow.recipeAdded', {}, 'Recipe added to workflow');
showToast(message, 'success');
} else {
showToast(`LoRA ${replaceMode ? 'replaced' : 'added'} to workflow`, 'success');
const message = replaceMode ?
translate('uiHelpers.workflow.loraReplaced', {}, 'LoRA replaced in workflow') :
translate('uiHelpers.workflow.loraAdded', {}, 'LoRA added to workflow');
showToast(message, 'success');
}
return true;
} else {
showToast(result.error || `Failed to send ${syntaxType === 'recipe' ? 'recipe' : 'LoRA'} to workflow`, 'error');
const errorMessage = result.error ||
(syntaxType === 'recipe' ?
translate('uiHelpers.workflow.recipeFailedToSend', {}, 'Failed to send recipe to workflow') :
translate('uiHelpers.workflow.loraFailedToSend', {}, 'Failed to send LoRA to workflow'));
showToast(errorMessage, 'error');
return false;
}
} catch (error) {
console.error('Failed to send to workflow:', error);
showToast(`Failed to send ${syntaxType === 'recipe' ? 'recipe' : 'LoRA'} to workflow`, 'error');
const message = syntaxType === 'recipe' ?
translate('uiHelpers.workflow.recipeFailedToSend', {}, 'Failed to send recipe to workflow') :
translate('uiHelpers.workflow.loraFailedToSend', {}, 'Failed to send LoRA to workflow');
showToast(message, 'error');
return false;
}
}
@@ -482,20 +501,26 @@ function showNodeSelector(nodes, loraSyntax, replaceMode, syntaxType) {
}).join('');
// Add header with action mode indicator
const actionType = syntaxType === 'recipe' ? 'Recipe' : 'LoRA';
const actionMode = replaceMode ? 'Replace' : 'Append';
const actionType = syntaxType === 'recipe' ?
translate('uiHelpers.nodeSelector.recipe', {}, 'Recipe') :
translate('uiHelpers.nodeSelector.lora', {}, 'LoRA');
const actionMode = replaceMode ?
translate('uiHelpers.nodeSelector.replace', {}, 'Replace') :
translate('uiHelpers.nodeSelector.append', {}, 'Append');
const selectTargetNodeText = translate('uiHelpers.nodeSelector.selectTargetNode', {}, 'Select target node');
const sendToAllText = translate('uiHelpers.nodeSelector.sendToAll', {}, 'Send to All');
selector.innerHTML = `
<div class="node-selector-header">
<span class="selector-action-type">${actionMode} ${actionType}</span>
<span class="selector-instruction">Select target node</span>
<span class="selector-instruction">${selectTargetNodeText}</span>
</div>
${nodeItems}
<div class="node-item send-all-item" data-action="send-all">
<div class="node-icon-indicator all-nodes">
<i class="fas fa-broadcast-tower"></i>
</div>
<span>Send to All</span>
<span>${sendToAllText}</span>
</div>
`;
@@ -654,15 +679,18 @@ export async function openExampleImagesFolder(modelHash) {
const result = await response.json();
if (result.success) {
showToast('Opening example images folder', 'success');
const message = translate('uiHelpers.exampleImages.openingFolder', {}, 'Opening example images folder');
showToast(message, 'success');
return true;
} else {
showToast(result.error || 'Failed to open example images folder', 'error');
const message = result.error || translate('uiHelpers.exampleImages.failedToOpen', {}, 'Failed to open example images folder');
showToast(message, 'error');
return false;
}
} catch (error) {
console.error('Failed to open example images folder:', error);
showToast('Failed to open example images folder', 'error');
const message = translate('uiHelpers.exampleImages.failedToOpen', {}, 'Failed to open example images folder');
showToast(message, 'error');
return false;
}
}