mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user