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

@@ -8,7 +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';
import { translate } from '../../utils/i18nHelpers.js';
// Add global event delegation handlers
export function setupModelCardEventDelegation(modelType) {
@@ -143,15 +143,15 @@ async function toggleFavorite(card) {
});
if (newFavoriteState) {
const addedText = safeTranslate('modelCard.favorites.added', {}, 'Added to favorites');
const addedText = translate('modelCard.favorites.added', {}, 'Added to favorites');
showToast(addedText, 'success');
} else {
const removedText = safeTranslate('modelCard.favorites.removed', {}, 'Removed from favorites');
const removedText = translate('modelCard.favorites.removed', {}, 'Removed from favorites');
showToast(removedText, 'success');
}
} catch (error) {
console.error('Failed to update favorite status:', error);
const errorText = safeTranslate('modelCard.favorites.updateFailed', {}, 'Failed to update favorite status');
const errorText = translate('modelCard.favorites.updateFailed', {}, 'Failed to update favorite status');
showToast(errorText, 'error');
}
}
@@ -164,7 +164,7 @@ function handleSendToWorkflow(card, replaceMode, modelType) {
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
} else {
// Checkpoint send functionality - to be implemented
const text = safeTranslate('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'Send checkpoint to workflow - feature to be implemented');
const text = translate('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'Send checkpoint to workflow - feature to be implemented');
showToast(text, 'info');
}
}
@@ -175,7 +175,8 @@ function handleCopyAction(card, modelType) {
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
// Checkpoint copy functionality - copy checkpoint name
const checkpointName = card.dataset.file_name;
copyToClipboard(checkpointName, 'Checkpoint name copied');
const message = translate('modelCard.actions.checkpointNameCopied', {}, 'Checkpoint name copied');
copyToClipboard(checkpointName, message);
} else if (modelType === MODEL_TYPES.EMBEDDING) {
const embeddingName = card.dataset.file_name;
copyToClipboard(embeddingName, 'Embedding name copied');
@@ -200,7 +201,7 @@ async function handleExampleImagesAccess(card, modelType) {
}
} catch (error) {
console.error('Error checking for example images:', error);
const text = safeTranslate('modelCard.exampleImages.checkError', {}, 'Error checking for example images');
const text = translate('modelCard.exampleImages.checkError', {}, 'Error checking for example images');
showToast(text, 'error');
}
}
@@ -283,7 +284,7 @@ function showExampleAccessModal(card, modelType) {
// Get the model hash
const modelHash = card.dataset.sha256;
if (!modelHash) {
const text = safeTranslate('modelCard.exampleImages.missingHash', {}, 'Missing model hash information.');
const text = translate('modelCard.exampleImages.missingHash', {}, 'Missing model hash information.');
showToast(text, 'error');
return;
}
@@ -305,7 +306,8 @@ function showExampleAccessModal(card, modelType) {
};
} else {
downloadBtn.classList.add('disabled');
downloadBtn.setAttribute('title', 'No remote example images available for this model on Civitai');
const noRemoteImagesTitle = translate('modelCard.exampleImages.noRemoteImagesAvailable', {}, 'No remote example images available for this model on Civitai');
downloadBtn.setAttribute('title', noRemoteImagesTitle);
downloadBtn.onclick = null;
}
}
@@ -436,14 +438,14 @@ export function createModelCard(model, modelType) {
const previewUrl = model.preview_url || '/loras_static/images/no-preview.png';
const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
// Determine NSFW warning text based on level
let nsfwText = "Mature Content";
// Determine NSFW warning text based on level with i18n support
let nsfwText = translate('modelCard.nsfw.matureContent', {}, 'Mature Content');
if (nsfwLevel >= NSFW_LEVELS.XXX) {
nsfwText = "XXX-rated Content";
nsfwText = translate('modelCard.nsfw.xxxRated', {}, 'XXX-rated Content');
} else if (nsfwLevel >= NSFW_LEVELS.X) {
nsfwText = "X-rated Content";
nsfwText = translate('modelCard.nsfw.xRated', {}, 'X-rated Content');
} else if (nsfwLevel >= NSFW_LEVELS.R) {
nsfwText = "R-rated Content";
nsfwText = translate('modelCard.nsfw.rRated', {}, 'R-rated Content');
}
// Check if autoplayOnHover is enabled for video previews
@@ -454,22 +456,36 @@ export function createModelCard(model, modelType) {
// Get favorite status from model data
const isFavorite = model.favorite === true;
// Generate action icons based on model type
// Generate action icons based on model type with i18n support
const favoriteTitle = isFavorite ?
translate('modelCard.actions.removeFromFavorites', {}, 'Remove from favorites') :
translate('modelCard.actions.addToFavorites', {}, 'Add to favorites');
const globeTitle = model.from_civitai ?
translate('modelCard.actions.viewOnCivitai', {}, 'View on Civitai') :
translate('modelCard.actions.notAvailableFromCivitai', {}, 'Not available from Civitai');
const sendTitle = translate('modelCard.actions.sendToWorkflow', {}, 'Send to ComfyUI (Click: Append, Shift+Click: Replace)');
const copyTitle = translate('modelCard.actions.copyLoRASyntax', {}, 'Copy LoRA Syntax');
const actionIcons = `
<i class="${isFavorite ? 'fas fa-star favorite-active' : 'far fa-star'}"
title="${isFavorite ? 'Remove from favorites' : 'Add to favorites'}">
title="${favoriteTitle}">
</i>
<i class="fas fa-globe"
title="${model.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
title="${globeTitle}"
${!model.from_civitai ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}>
</i>
<i class="fas fa-paper-plane"
title="Send to ComfyUI (Click: Append, Shift+Click: Replace)">
title="${sendTitle}">
</i>
<i class="fas fa-copy"
title="Copy LoRA Syntax">
title="${copyTitle}">
</i>`;
// Generate UI text with i18n support
const toggleBlurTitle = translate('modelCard.actions.toggleBlur', {}, 'Toggle blur');
const showButtonText = translate('modelCard.actions.show', {}, 'Show');
const openExampleImagesTitle = translate('modelCard.actions.openExampleImages', {}, 'Open Example Images Folder');
card.innerHTML = `
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
${isVideo ?
@@ -480,7 +496,7 @@ export function createModelCard(model, modelType) {
}
<div class="card-header">
${shouldBlur ?
`<button class="toggle-blur-btn" title="Toggle blur">
`<button class="toggle-blur-btn" title="${toggleBlurTitle}">
<i class="fas fa-eye"></i>
</button>` : ''}
<span class="base-model-label ${shouldBlur ? 'with-toggle' : ''}" title="${model.base_model}">
@@ -494,7 +510,7 @@ export function createModelCard(model, modelType) {
<div class="nsfw-overlay">
<div class="nsfw-warning">
<p>${nsfwText}</p>
<button class="show-content-btn">Show</button>
<button class="show-content-btn">${showButtonText}</button>
</div>
</div>
` : ''}
@@ -505,7 +521,7 @@ export function createModelCard(model, modelType) {
</div>
<div class="card-actions">
<i class="fas fa-folder-open"
title="Open Example Images Folder">
title="${openExampleImagesTitle}">
</i>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { showToast } from '../../utils/uiHelpers.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
/**
* ModelDescription.js
@@ -63,7 +63,7 @@ async function loadModelDescription() {
const description = await getModelApiClient().fetchModelDescription(filePath);
// Update content
const noDescriptionText = safeTranslate('modals.model.description.noDescription', {}, 'No model description available');
const noDescriptionText = translate('modals.model.description.noDescription', {}, 'No model description available');
descriptionContent.innerHTML = description || `<div class="no-description">${noDescriptionText}</div>`;
descriptionContent.dataset.loaded = 'true';
@@ -72,7 +72,7 @@ async function loadModelDescription() {
} catch (error) {
console.error('Error loading model description:', error);
const failedText = safeTranslate('modals.model.description.failedToLoad', {}, 'Failed to load model description');
const failedText = translate('modals.model.description.failedToLoad', {}, 'Failed to load model description');
descriptionContent.innerHTML = `<div class="no-description">${failedText}</div>`;
} finally {
// Hide loading state
@@ -96,7 +96,7 @@ export async function setupModelDescriptionEditing(filePath) {
editBtn = document.createElement('button');
editBtn.className = 'edit-model-description-btn';
// Set title using i18n
const editTitle = safeTranslate('modals.model.description.editTitle', {}, 'Edit model description');
const editTitle = translate('modals.model.description.editTitle', {}, 'Edit model description');
editBtn.title = editTitle;
editBtn.innerHTML = '<i class="fas fa-pencil-alt"></i>';
descContainer.insertBefore(editBtn, descContent);
@@ -154,7 +154,7 @@ export async function setupModelDescriptionEditing(filePath) {
}
if (!newValue) {
this.innerHTML = originalValue;
const emptyErrorText = safeTranslate('modals.model.description.validation.cannotBeEmpty', {}, 'Description cannot be empty');
const emptyErrorText = translate('modals.model.description.validation.cannotBeEmpty', {}, 'Description cannot be empty');
showToast(emptyErrorText, 'error');
exitEditMode();
return;
@@ -163,11 +163,11 @@ export async function setupModelDescriptionEditing(filePath) {
// Save to backend
const { getModelApiClient } = await import('../../api/modelApiFactory.js');
await getModelApiClient().saveModelMetadata(filePath, { modelDescription: newValue });
const successText = safeTranslate('modals.model.description.messages.updated', {}, 'Model description updated');
const successText = translate('modals.model.description.messages.updated', {}, 'Model description updated');
showToast(successText, 'success');
} catch (err) {
this.innerHTML = originalValue;
const errorText = safeTranslate('modals.model.description.messages.updateFailed', {}, 'Failed to update model description');
const errorText = translate('modals.model.description.messages.updateFailed', {}, 'Failed to update model description');
showToast(errorText, 'error');
} finally {
exitEditMode();

View File

@@ -5,7 +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';
import { translate } from '../../utils/i18nHelpers.js';
/**
* Set up model name editing functionality
@@ -83,7 +83,7 @@ export function setupModelNameEditing(filePath) {
sel.removeAllRanges();
sel.addRange(range);
const text = safeTranslate('modelMetadata.validation.nameTooLong', {}, 'Model name is limited to 100 characters');
const text = translate('modelMetadata.validation.nameTooLong', {}, 'Model name is limited to 100 characters');
showToast(text, 'warning');
}
});

View File

@@ -18,7 +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';
import { translate } from '../../utils/i18nHelpers.js';
/**
* Display the model modal with the given model data
@@ -62,9 +62,9 @@ export async function showModelModal(model, modelType) {
}
// Generate tabs based on model type
const examplesText = safeTranslate('modals.model.tabs.examples', {}, 'Examples');
const descriptionText = safeTranslate('modals.model.tabs.description', {}, 'Model Description');
const recipesText = safeTranslate('modals.model.tabs.recipes', {}, 'Recipes');
const examplesText = translate('modals.model.tabs.examples', {}, 'Examples');
const descriptionText = translate('modals.model.tabs.description', {}, 'Model Description');
const recipesText = translate('modals.model.tabs.recipes', {}, 'Recipes');
const tabsContent = modelType === 'loras' ?
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
@@ -73,10 +73,10 @@ export async function showModelModal(model, modelType) {
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
<button class="tab-btn" data-tab="description">${descriptionText}</button>`;
const loadingExampleImagesText = safeTranslate('modals.model.loading.exampleImages', {}, 'Loading example images...');
const loadingDescriptionText = safeTranslate('modals.model.loading.description', {}, 'Loading model description...');
const loadingRecipesText = safeTranslate('modals.model.loading.recipes', {}, 'Loading recipes...');
const loadingExamplesText = safeTranslate('modals.model.loading.examples', {}, 'Loading examples...');
const loadingExampleImagesText = translate('modals.model.loading.exampleImages', {}, 'Loading example images...');
const loadingDescriptionText = translate('modals.model.loading.description', {}, 'Loading model description...');
const loadingRecipesText = translate('modals.model.loading.recipes', {}, 'Loading recipes...');
const loadingExamplesText = translate('modals.model.loading.examples', {}, 'Loading examples...');
const tabPanesContent = modelType === 'loras' ?
`<div id="showcase-tab" class="tab-pane active">
@@ -122,19 +122,19 @@ export async function showModelModal(model, modelType) {
<header class="modal-header">
<div class="model-name-header">
<h2 class="model-name-content">${modalTitle}</h2>
<button class="edit-model-name-btn" title="Edit model name">
<button class="edit-model-name-btn" title="${translate('modals.model.actions.editModelName', {}, 'Edit model name')}">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
<div class="creator-actions">
${modelWithFullData.from_civitai ? `
<div class="civitai-view" title="View on Civitai" data-action="view-civitai" data-filepath="${modelWithFullData.file_path}">
<i class="fas fa-globe"></i> View on Civitai
<div class="civitai-view" title="${translate('modals.model.actions.viewOnCivitai', {}, 'View on Civitai')}" data-action="view-civitai" data-filepath="${modelWithFullData.file_path}">
<i class="fas fa-globe"></i> ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')}
</div>` : ''}
${modelWithFullData.civitai?.creator ? `
<div class="creator-info" data-username="${modelWithFullData.civitai.creator.username}" data-action="view-creator" title="View Creator Profile">
<div class="creator-info" data-username="${modelWithFullData.civitai.creator.username}" data-action="view-creator" title="${translate('modals.model.actions.viewCreatorProfile', {}, 'View Creator Profile')}">
${modelWithFullData.civitai.creator.image ?
`<div class="creator-avatar">
<img src="${modelWithFullData.civitai.creator.image}" alt="${modelWithFullData.civitai.creator.username}" onerror="this.onerror=null; this.src='static/icons/user-placeholder.png';">
@@ -154,48 +154,48 @@ export async function showModelModal(model, modelType) {
<div class="info-section">
<div class="info-grid">
<div class="info-item">
<label>Version</label>
<label>${translate('modals.model.metadata.version', {}, 'Version')}</label>
<span>${modelWithFullData.civitai?.name || 'N/A'}</span>
</div>
<div class="info-item">
<label>File Name</label>
<label>${translate('modals.model.metadata.fileName', {}, 'File Name')}</label>
<div class="file-name-wrapper">
<span id="file-name" class="file-name-content">${modelWithFullData.file_name || 'N/A'}</span>
<button class="edit-file-name-btn" title="Edit file name">
<button class="edit-file-name-btn" title="${translate('modals.model.actions.editFileName', {}, 'Edit file name')}">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
</div>
<div class="info-item location-size">
<div class="location-wrapper">
<label>Location</label>
<label>${translate('modals.model.metadata.location', {}, 'Location')}</label>
<span class="file-path">${modelWithFullData.file_path.replace(/[^/]+$/, '') || 'N/A'}</span>
</div>
</div>
<div class="info-item base-size">
<div class="base-wrapper">
<label>Base Model</label>
<label>${translate('modals.model.metadata.baseModel', {}, 'Base Model')}</label>
<div class="base-model-display">
<span class="base-model-content">${modelWithFullData.base_model || 'Unknown'}</span>
<button class="edit-base-model-btn" title="Edit base model">
<span class="base-model-content">${modelWithFullData.base_model || translate('modals.model.metadata.unknown', {}, 'Unknown')}</span>
<button class="edit-base-model-btn" title="${translate('modals.model.actions.editBaseModel', {}, 'Edit base model')}">
<i class="fas fa-pencil-alt"></i>
</button>
</div>
</div>
<div class="size-wrapper">
<label>Size</label>
<label>${translate('modals.model.metadata.size', {}, 'Size')}</label>
<span>${formatFileSize(modelWithFullData.file_size)}</span>
</div>
</div>
${typeSpecificContent}
<div class="info-item notes">
<label>Additional Notes <i class="fas fa-info-circle notes-hint" title="Press Enter to save, Shift+Enter for new line"></i></label>
<label>${translate('modals.model.metadata.additionalNotes', {}, 'Additional Notes')} <i class="fas fa-info-circle notes-hint" title="${translate('modals.model.metadata.notesHint', {}, 'Press Enter to save, Shift+Enter for new line')}"></i></label>
<div class="editable-field">
<div class="notes-content" contenteditable="true" spellcheck="false">${modelWithFullData.notes || 'Add your notes here...'}</div>
<div class="notes-content" contenteditable="true" spellcheck="false">${modelWithFullData.notes || translate('modals.model.metadata.addNotesPlaceholder', {}, 'Add your notes here...')}</div>
</div>
</div>
<div class="info-item full-width">
<label>About this version</label>
<label>${translate('modals.model.metadata.aboutThisVersion', {}, 'About this version')}</label>
<div class="description-text">${modelWithFullData.civitai?.description || 'N/A'}</div>
</div>
</div>
@@ -259,18 +259,18 @@ export async function showModelModal(model, modelType) {
function renderLoraSpecificContent(lora, escapedWords) {
return `
<div class="info-item usage-tips">
<label>Usage Tips</label>
<label>${translate('modals.model.metadata.usageTips', {}, 'Usage Tips')}</label>
<div class="editable-field">
<div class="preset-controls">
<select id="preset-selector">
<option value="">Add preset parameter...</option>
<option value="strength_min">Strength Min</option>
<option value="strength_max">Strength Max</option>
<option value="strength">Strength</option>
<option value="clip_skip">Clip Skip</option>
<option value="">${translate('modals.model.usageTips.addPresetParameter', {}, 'Add preset parameter...')}</option>
<option value="strength_min">${translate('modals.model.usageTips.strengthMin', {}, 'Strength Min')}</option>
<option value="strength_max">${translate('modals.model.usageTips.strengthMax', {}, 'Strength Max')}</option>
<option value="strength">${translate('modals.model.usageTips.strength', {}, 'Strength')}</option>
<option value="clip_skip">${translate('modals.model.usageTips.clipSkip', {}, 'Clip Skip')}</option>
</select>
<input type="number" id="preset-value" step="0.01" placeholder="Value" style="display:none;">
<button class="add-preset-btn">Add</button>
<input type="number" id="preset-value" step="0.01" placeholder="${translate('modals.model.usageTips.valuePlaceholder', {}, 'Value')}" style="display:none;">
<button class="add-preset-btn">${translate('modals.model.usageTips.add', {}, 'Add')}</button>
</div>
<div class="preset-tags">
${renderPresetTags(parsePresets(lora.usage_tips))}
@@ -438,9 +438,11 @@ async function saveNotes(filePath) {
try {
await getModelApiClient().saveModelMetadata(filePath, { notes: content });
showToast('Notes saved successfully', 'success');
const successMessage = translate('modals.model.notes.saved', {}, 'Notes saved successfully');
showToast(successMessage, 'success');
} catch (error) {
showToast('Failed to save notes', 'error');
const errorMessage = translate('modals.model.notes.saveFailed', {}, 'Failed to save notes');
showToast(errorMessage, 'error');
}
}

View File

@@ -4,7 +4,7 @@
*/
import { showToast } from '../../utils/uiHelpers.js';
import { getModelApiClient } from '../../api/modelApiFactory.js';
import { safeTranslate } from '../../utils/i18nHelpers.js';
import { translate } from '../../utils/i18nHelpers.js';
// Preset tag suggestions
const PRESET_TAGS = [
@@ -217,10 +217,10 @@ async function saveTags() {
// Exit edit mode
editBtn.click();
showToast(safeTranslate('modelTags.messages.updated', {}, 'Tags updated successfully'), 'success');
showToast(translate('modelTags.messages.updated', {}, 'Tags updated successfully'), 'success');
} catch (error) {
console.error('Error saving tags:', error);
showToast(safeTranslate('modelTags.messages.updateFailed', {}, 'Failed to update tags'), 'error');
showToast(translate('modelTags.messages.updateFailed', {}, 'Failed to update tags'), 'error');
}
}
@@ -362,7 +362,7 @@ function addNewTag(tag) {
// Validation: Check length
if (tag.length > 30) {
const text = safeTranslate('modelTags.validation.maxLength', {}, 'Tag should not exceed 30 characters');
const text = translate('modelTags.validation.maxLength', {}, 'Tag should not exceed 30 characters');
showToast(text, 'error');
return;
}
@@ -370,7 +370,7 @@ function addNewTag(tag) {
// Validation: Check total number
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
if (currentTags.length >= 30) {
const text = safeTranslate('modelTags.validation.maxCount', {}, 'Maximum 30 tags allowed');
const text = translate('modelTags.validation.maxCount', {}, 'Maximum 30 tags allowed');
showToast(text, 'error');
return;
}
@@ -378,7 +378,7 @@ function addNewTag(tag) {
// Validation: Check for duplicates
const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
if (existingTags.includes(tag)) {
const text = safeTranslate('modelTags.validation.duplicate', {}, 'This tag already exists');
const text = translate('modelTags.validation.duplicate', {}, 'This tag already exists');
showToast(text, 'error');
return;
}

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