diff --git a/locales/en.json b/locales/en.json index 5935f785..6d1cbf03 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" + } } } diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 63718e4f..eb882abd 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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": "在此位置加载项目失败" + } } } diff --git a/static/js/core.js b/static/js/core.js index 6f659a71..542727dc 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -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 diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 2defae45..0f75190a 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -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; } diff --git a/static/js/managers/SearchManager.js b/static/js/managers/SearchManager.js index 5ff1e81c..83321465 100644 --- a/static/js/managers/SearchManager.js +++ b/static/js/managers/SearchManager.js @@ -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; } diff --git a/static/js/statistics.js b/static/js/statistics.js index 80646dd8..4b79934b 100644 --- a/static/js/statistics.js +++ b/static/js/statistics.js @@ -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'); } } diff --git a/static/js/utils/VirtualScroller.js b/static/js/utils/VirtualScroller.js index 0e3cf49c..57f46c06 100644 --- a/static/js/utils/VirtualScroller.js +++ b/static/js/utils/VirtualScroller.js @@ -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; } diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index 8ad852ee..b97accb8 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -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 = ` diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 3a35debd..c8981e9b 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -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; } } \ No newline at end of file