feat(localization): enhance toast messages for better user feedback and localization support

This commit is contained in:
Will Miao
2025-08-31 11:51:28 +08:00
parent a258a18fa4
commit be8edafed0
9 changed files with 268 additions and 50 deletions

View File

@@ -703,6 +703,7 @@
"sendToAll": "Send to All"
},
"exampleImages": {
"opened": "Example images folder opened",
"openingFolder": "Opening example images folder",
"failedToOpen": "Failed to open example images folder"
}
@@ -764,5 +765,164 @@
"title": "Provide Feedback",
"description": "Your feedback helps shape future updates! Share your thoughts:"
}
},
"toast": {
"general": {
"cannotInteractStandalone": "Cannot interact with ComfyUI in standalone mode",
"failedWorkflowInfo": "Failed to get workflow information",
"pageInitFailed": "Failed to initialize {pageType} page. Please reload.",
"statisticsLoadFailed": "Failed to load statistics data",
"unexpectedError": "An unexpected error occurred"
},
"loras": {
"fetchFromCivitai": "Fetch from Civitai",
"downloadFromUrl": "Download from URL",
"copyOnlyForLoras": "Copy syntax is only available for LoRAs",
"noLorasSelected": "No LoRAs selected",
"missingDataForLoras": "Missing data for {count} LoRAs",
"noValidLorasToCopy": "No valid LoRAs to copy",
"sendOnlyForLoras": "Send to workflow is only available for LoRAs",
"noValidLorasToSend": "No valid LoRAs to send",
"syntaxCopiedWithGroups": "LoRA syntax with trigger word groups copied to clipboard",
"downloadSuccessful": "LoRAs downloaded successfully",
"allDownloadSuccessful": "All {count} LoRAs downloaded successfully",
"downloadPartialSuccess": "Downloaded {completed} of {total} LoRAs"
},
"recipes": {
"nameSaved": "Recipe \"{name}\" saved successfully",
"nameUpdated": "Recipe name updated successfully",
"tagsUpdated": "Recipe tags updated successfully",
"sourceUrlUpdated": "Source URL updated successfully",
"noRecipeId": "No recipe ID available",
"copyFailed": "Error copying recipe syntax: {message}",
"noMissingLoras": "No missing LoRAs to download",
"missingLorasInfoFailed": "Failed to get information for missing LoRAs",
"preparingForDownloadFailed": "Error preparing LoRAs for download",
"enterLoraName": "Please enter a LoRA name or syntax",
"reconnectedSuccessfully": "LoRA reconnected successfully",
"reconnectFailed": "Error reconnecting LoRA: {message}",
"cannotSend": "Cannot send recipe: Missing recipe ID",
"sendFailed": "Failed to send recipe to workflow",
"sendError": "Error sending recipe to workflow",
"cannotDelete": "Cannot delete recipe: Missing recipe ID",
"deleteConfirmationError": "Error showing delete confirmation",
"deletedSuccessfully": "Recipe deleted successfully",
"deleteFailed": "Error deleting recipe: {message}",
"cannotShare": "Cannot share recipe: Missing recipe ID",
"preparingForSharing": "Preparing recipe for sharing...",
"downloadStarted": "Recipe download started",
"shareError": "Error sharing recipe: {message}",
"sharePreparationError": "Error preparing recipe for sharing"
},
"models": {
"noModelsSelected": "No models selected",
"deletedSuccessfully": "Successfully deleted {count} {type}(s)",
"deleteFailed": "Error: {error}",
"deleteFailedGeneral": "Failed to delete models",
"selectedAdditional": "Selected {count} additional {type}(s)",
"refreshMetadataFailed": "Failed to refresh metadata",
"nameCannotBeEmpty": "Model name cannot be empty",
"nameUpdatedSuccessfully": "Model name updated successfully",
"nameUpdateFailed": "Failed to update model name",
"baseModelUpdated": "Base model updated successfully",
"baseModelUpdateFailed": "Failed to update base model",
"invalidCharactersRemoved": "Invalid characters removed from filename",
"filenameCannotBeEmpty": "File name cannot be empty",
"renameFailed": "Failed to rename file: {message}",
"moveFailed": "Failed to move model(s): {message}",
"pleaseSelectRoot": "Please select a {type} root directory"
},
"search": {
"atLeastOneOption": "At least one search option must be selected"
},
"settings": {
"loraRootsFailed": "Failed to load LoRA roots: {message}",
"checkpointRootsFailed": "Failed to load checkpoint roots: {message}",
"embeddingRootsFailed": "Failed to load embedding roots: {message}",
"mappingsUpdated": "Base model path mappings updated ({count} mapping{plural})",
"mappingsCleared": "Base model path mappings cleared",
"mappingSaveFailed": "Failed to save base model mappings: {message}",
"downloadTemplatesUpdated": "Download path templates updated",
"downloadTemplatesFailed": "Failed to save download path templates: {message}",
"settingsUpdated": "Settings updated: {setting}",
"compactModeToggled": "Compact Mode {state}",
"compactModeEnabled": "enabled",
"compactModeDisabled": "disabled",
"settingSaveFailed": "Failed to save setting: {message}",
"displayDensitySet": "Display Density set to {density}",
"languageChangeFailed": "Failed to change language: {message}",
"cacheCleared": "Cache files have been cleared successfully. Cache will rebuild on next action.",
"cacheClearFailed": "Failed to clear cache: {error}",
"cacheClearError": "Error clearing cache: {message}"
},
"filters": {
"applied": "Filters applied - showing {count} {type}",
"cleared": "Filters cleared"
},
"downloads": {
"selectVersion": "Please select a version",
"versionExists": "This version already exists in your library",
"completed": "Download completed successfully",
"alreadyInProgress": "Download already in progress",
"enterLocationFirst": "Please enter a download location first",
"started": "Example images download started",
"startFailed": "Failed to start download",
"paused": "Download paused",
"pauseFailed": "Failed to pause download",
"resumed": "Download resumed",
"resumeFailed": "Failed to resume download",
"imagesCompleted": "Example images {action} completed",
"imagesFailed": "Example images {action} failed"
},
"import": {
"enterRecipeName": "Please enter a recipe name",
"selectImageFirst": "Please select an image first",
"folderTreeFailed": "Failed to load folder tree",
"folderTreeError": "Error loading folder tree",
"imagesImported": "Example images imported successfully",
"importFailed": "Failed to import example images: {message}"
},
"triggerWords": {
"loadFailed": "Could not load trained words",
"tooLong": "Trigger word should not exceed 30 words",
"tooMany": "Maximum 30 trigger words allowed",
"alreadyExists": "This trigger word already exists",
"updated": "Trigger words updated successfully",
"updateFailed": "Failed to update trigger words",
"copyFailed": "Copy failed"
},
"examples": {
"pathUpdated": "Example images path updated successfully",
"deleted": "Example image deleted",
"deleteFailed": "Failed to delete example image",
"setPreviewFailed": "Failed to set preview image"
},
"virtual": {
"loadFailed": "Failed to load items",
"loadMoreFailed": "Failed to load more items",
"loadPositionFailed": "Failed to load items at this position"
},
"bulk": {
"unableToSelectAll": "Unable to select all items"
},
"tags": {
"tagTooLong": "Tag is too long (max {max} characters)",
"tooManyTags": "Too many tags (max {max} tags)",
"tagAlreadyExists": "Tag already exists"
},
"favorites": {
"added": "Added to favorites",
"removed": "Removed from favorites",
"updateFailed": "Failed to update favorite status"
},
"workflow": {
"checkpointNotImplemented": "Send checkpoint to workflow - feature to be implemented",
"failedToSend": "Failed to send LoRA to workflow"
},
"exampleImages": {
"checkError": "Error checking for example images",
"missingHash": "Missing model hash information.",
"noRemoteImages": "No remote example images available for this model on Civitai"
}
}
}

View File

@@ -730,5 +730,71 @@
"title": "提供反馈",
"description": "您的反馈有助于塑造未来的更新!分享您的想法:"
}
},
"uiHelpers": {
"clipboard": {
"copied": "已复制到剪贴板",
"copyFailed": "复制失败"
},
"workflow": {
"noSupportedNodes": "工作流中未找到支持的目标节点",
"communicationFailed": "与 ComfyUI 通信失败",
"loraAdded": "LoRA 已添加到工作流",
"loraReplaced": "LoRA 已在工作流中替换",
"recipeAdded": "配方已添加到工作流",
"recipeReplaced": "配方已在工作流中替换",
"loraFailedToSend": "发送 LoRA 到工作流失败",
"recipeFailedToSend": "发送配方到工作流失败"
},
"nodeSelector": {
"recipe": "配方",
"lora": "LoRA",
"replace": "替换",
"append": "追加",
"selectTargetNode": "选择目标节点",
"sendToAll": "发送到全部"
},
"exampleImages": {
"opened": "示例图片文件夹已打开",
"openingFolder": "正在打开示例图片文件夹",
"failedToOpen": "打开示例图片文件夹失败"
}
},
"toast": {
"general": {
"cannotInteractStandalone": "无法在独立模式下与 ComfyUI 交互",
"failedWorkflowInfo": "获取工作流信息失败",
"pageInitFailed": "初始化 {pageType} 页面失败。请重新加载。",
"statisticsLoadFailed": "加载统计数据失败",
"unexpectedError": "发生意外错误"
},
"loras": {
"copyOnlyForLoras": "复制语法仅适用于 LoRA",
"noLorasSelected": "未选择任何 LoRA",
"missingDataForLoras": "{count} 个 LoRA 缺少数据",
"noValidLorasToCopy": "没有有效的 LoRA 可复制",
"sendOnlyForLoras": "发送到工作流仅适用于 LoRA",
"noValidLorasToSend": "没有有效的 LoRA 可发送",
"syntaxCopiedWithGroups": "LoRA 语法与触发词组已复制到剪贴板",
"downloadSuccessful": "LoRA 下载成功",
"allDownloadSuccessful": "所有 {count} 个 LoRA 下载成功",
"downloadPartialSuccess": "已下载 {completed} / {total} 个 LoRA"
},
"models": {
"noModelsSelected": "未选择任何模型",
"deletedSuccessfully": "成功删除 {count} 个{type}",
"deleteFailed": "错误:{error}",
"deleteFailedGeneral": "删除模型失败",
"selectedAdditional": "额外选择了 {count} 个{type}",
"refreshMetadataFailed": "刷新元数据失败"
},
"search": {
"atLeastOneOption": "至少需要选择一个搜索选项"
},
"virtual": {
"loadFailed": "加载项目失败",
"loadMoreFailed": "加载更多项目失败",
"loadPositionFailed": "在此位置加载项目失败"
}
}
}

View File

@@ -76,8 +76,8 @@ export class AppCore {
}
// Show toast messages
showToast(message, type = 'info') {
showToast(message, type);
showToast(key, params = {}, type = 'info') {
showToast(key, params, type);
}
// Initialize common UI features based on page type

View File

@@ -283,12 +283,12 @@ export class BulkManager {
async copyAllModelsSyntax() {
if (state.currentPageType !== MODEL_TYPES.LORA) {
showToast('Copy syntax is only available for LoRAs', 'warning');
showToast('toast.loras.copyOnlyForLoras', {}, 'warning');
return;
}
if (state.selectedModels.size === 0) {
showToast('No LoRAs selected', 'warning');
showToast('toast.loras.noLorasSelected', {}, 'warning');
return;
}
@@ -310,11 +310,11 @@ export class BulkManager {
if (missingLoras.length > 0) {
console.warn('Missing metadata for some selected loras:', missingLoras);
showToast(`Missing data for ${missingLoras.length} LoRAs`, 'warning');
showToast('toast.loras.missingDataForLoras', { count: missingLoras.length }, 'warning');
}
if (loraSyntaxes.length === 0) {
showToast('No valid LoRAs to copy', 'error');
showToast('toast.loras.noValidLorasToCopy', {}, 'error');
return;
}
@@ -323,12 +323,12 @@ export class BulkManager {
async sendAllModelsToWorkflow() {
if (state.currentPageType !== MODEL_TYPES.LORA) {
showToast('Send to workflow is only available for LoRAs', 'warning');
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
return;
}
if (state.selectedModels.size === 0) {
showToast('No LoRAs selected', 'warning');
showToast('toast.loras.noLorasSelected', {}, 'warning');
return;
}

View File

@@ -1,4 +1,4 @@
import { updatePanelPositions } from "../utils/uiHelpers.js";
import { updatePanelPositions, showToast } from "../utils/uiHelpers.js";
import { getCurrentPageState } from "../state/index.js";
import { getModelApiClient } from "../api/modelApiFactory.js";
import { setStorageItem, getStorageItem } from "../utils/storageHelpers.js";
@@ -97,10 +97,7 @@ export class SearchManager {
// Check if clicking would deselect the last active option
const activeOptions = document.querySelectorAll('.search-option-tag.active');
if (activeOptions.length === 1 && activeOptions[0] === tag) {
// Don't allow deselecting the last option
if (typeof showToast === 'function') {
showToast('At least one search option must be selected', 'info');
}
showToast('toast.search.atLeastOneOption', {}, 'info');
return;
}

View File

@@ -85,7 +85,7 @@ class StatisticsManager {
console.log('Statistics data loaded:', this.data);
} catch (error) {
console.error('Error loading statistics data:', error);
showToast('Failed to load statistics data', 'error');
showToast('toast.general.statisticsLoadFailed', {}, 'error');
}
}

View File

@@ -210,7 +210,7 @@ export class VirtualScroller {
this.scheduleRender();
} catch (err) {
console.error('Failed to initialize virtual scroller:', err);
showToast('Failed to load items', 'error');
showToast('toast.virtual.loadFailed', {}, 'error');
}
}
@@ -293,7 +293,7 @@ export class VirtualScroller {
return items;
} catch (err) {
console.error('Failed to load more items:', err);
showToast('Failed to load more items', 'error');
showToast('toast.virtual.loadMoreFailed', {}, 'error');
} finally {
this.isLoading = false;
pageState.isLoading = false;
@@ -571,7 +571,7 @@ export class VirtualScroller {
}
} catch (err) {
console.error('Failed to fetch data window:', err);
showToast('Failed to load items at this position', 'error');
showToast('toast.virtual.loadPositionFailed', {}, 'error');
} finally {
this.fetchingWindow = false;
}

View File

@@ -129,7 +129,7 @@ async function initializeVirtualScroll(pageType) {
} catch (error) {
console.error(`Error initializing virtual scroller for ${pageType}:`, error);
showToast(`Failed to initialize ${pageType} page. Please reload.`, 'error');
showToast('toast.general.pageInitFailed', { pageType }, 'error');
// Fallback: show a message in the grid
grid.innerHTML = `

View File

@@ -34,18 +34,18 @@ export async function copyToClipboard(text, successMessage = null) {
}
if (defaultSuccessMessage) {
showToast(defaultSuccessMessage, 'success');
showToast('uiHelpers.clipboard.copied', {}, 'success');
}
return true;
} catch (err) {
console.error('Copy failed:', err);
const errorMessage = translate('uiHelpers.clipboard.copyFailed', {}, 'Copy failed');
showToast(errorMessage, 'error');
showToast('uiHelpers.clipboard.copyFailed', {}, 'error');
return false;
}
}
export function showToast(message, type = 'info') {
export function showToast(key, params = {}, type = 'info') {
const message = translate(key, params);
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
@@ -376,11 +376,11 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
// Handle specific error cases
if (registryData.error === 'Standalone Mode Active') {
// Standalone mode - show warning with specific message
showToast(registryData.message || 'Cannot interact with ComfyUI in standalone mode', 'warning');
showToast('toast.general.cannotInteractStandalone', {}, 'warning');
return false;
} else {
// Other errors - show error toast
showToast(registryData.message || registryData.error || 'Failed to get workflow information', 'error');
showToast('toast.general.failedWorkflowInfo', {}, 'error');
return false;
}
}
@@ -388,8 +388,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
// Success case - check node count
if (registryData.data.node_count === 0) {
// No nodes found - show warning
const message = translate('uiHelpers.workflow.noSupportedNodes', {}, 'No supported target nodes found in workflow');
showToast(message, 'warning');
showToast('uiHelpers.workflow.noSupportedNodes', {}, 'warning');
return false;
} else if (registryData.data.node_count > 1) {
// Multiple nodes - show selector
@@ -402,8 +401,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
}
} catch (error) {
console.error('Failed to get registry:', error);
const message = translate('uiHelpers.workflow.communicationFailed', {}, 'Failed to communicate with ComfyUI');
showToast(message, 'error');
showToast('uiHelpers.workflow.communicationFailed', {}, 'error');
return false;
}
}
@@ -435,31 +433,30 @@ async function sendToSpecificNode(nodeIds, loraSyntax, replaceMode, syntaxType)
if (result.success) {
// Use different toast messages based on syntax type
if (syntaxType === 'recipe') {
const message = replaceMode ?
translate('uiHelpers.workflow.recipeReplaced', {}, 'Recipe replaced in workflow') :
translate('uiHelpers.workflow.recipeAdded', {}, 'Recipe added to workflow');
showToast(message, 'success');
const messageKey = replaceMode ?
'uiHelpers.workflow.recipeReplaced' :
'uiHelpers.workflow.recipeAdded';
showToast(messageKey, {}, 'success');
} else {
const message = replaceMode ?
translate('uiHelpers.workflow.loraReplaced', {}, 'LoRA replaced in workflow') :
translate('uiHelpers.workflow.loraAdded', {}, 'LoRA added to workflow');
showToast(message, 'success');
const messageKey = replaceMode ?
'uiHelpers.workflow.loraReplaced' :
'uiHelpers.workflow.loraAdded';
showToast(messageKey, {}, 'success');
}
return true;
} else {
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');
const messageKey = syntaxType === 'recipe' ?
'uiHelpers.workflow.recipeFailedToSend' :
'toast.workflow.failedToSend';
showToast(messageKey, {}, 'error');
return false;
}
} catch (error) {
console.error('Failed to send 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');
const messageKey = syntaxType === 'recipe' ?
'uiHelpers.workflow.recipeFailedToSend' :
'toast.workflow.failedToSend';
showToast(messageKey, {}, 'error');
return false;
}
}
@@ -680,17 +677,15 @@ export async function openExampleImagesFolder(modelHash) {
if (result.success) {
const message = translate('uiHelpers.exampleImages.openingFolder', {}, 'Opening example images folder');
showToast(message, 'success');
showToast('uiHelpers.exampleImages.opened', {}, 'success');
return true;
} else {
const message = result.error || translate('uiHelpers.exampleImages.failedToOpen', {}, 'Failed to open example images folder');
showToast(message, 'error');
showToast('uiHelpers.exampleImages.failedToOpen', {}, 'error');
return false;
}
} catch (error) {
console.error('Failed to open example images folder:', error);
const message = translate('uiHelpers.exampleImages.failedToOpen', {}, 'Failed to open example images folder');
showToast(message, 'error');
showToast('uiHelpers.exampleImages.failedToOpen', {}, 'error');
return false;
}
}