From 92341111ad9211f199c89dbc81862d0a835c3613 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 31 Aug 2025 22:41:35 +0800 Subject: [PATCH] feat(localization): enhance import modal and related components with new labels, descriptions, and error messages for improved user experience --- locales/en.json | 71 ++++++++++++++++--- locales/zh-CN.json | 71 ++++++++++++++++--- static/js/managers/ImportManager.js | 17 +++-- static/js/managers/import/DownloadManager.js | 13 ++-- static/js/managers/import/FolderBrowser.js | 5 +- static/js/managers/import/ImageProcessor.js | 11 +-- .../js/managers/import/RecipeDataManager.js | 47 ++++++------ templates/components/import_modal.html | 64 ++++++++--------- 8 files changed, 206 insertions(+), 93 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4d8ebd76..97c7f78b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -290,7 +290,59 @@ "controls": { "import": { "action": "Import Recipe", - "title": "Import a recipe from image or URL" + "title": "Import a recipe from image or URL", + "urlLocalPath": "URL / Local Path", + "uploadImage": "Upload Image", + "urlSectionDescription": "Input a Civitai image URL or local file path to import as a recipe.", + "imageUrlOrPath": "Image URL or File Path:", + "urlPlaceholder": "https://civitai.com/images/... or C:/path/to/image.png", + "fetchImage": "Fetch Image", + "uploadSectionDescription": "Upload an image with LoRA metadata to import as a recipe.", + "selectImage": "Select Image", + "recipeName": "Recipe Name", + "recipeNamePlaceholder": "Enter recipe name", + "tagsOptional": "Tags (optional)", + "addTagPlaceholder": "Add a tag", + "addTag": "Add", + "noTagsAdded": "No tags added", + "lorasInRecipe": "LoRAs in this Recipe", + "downloadLocationPreview": "Download Location Preview:", + "useDefaultPath": "Use Default Path", + "useDefaultPathTooltip": "When enabled, files are automatically organized using configured path templates", + "selectLoraRoot": "Select a LoRA root directory", + "targetFolderPath": "Target Folder Path:", + "folderPathPlaceholder": "Type folder path or select from tree below...", + "createNewFolder": "Create new folder", + "root": "Root", + "browseFolders": "Browse Folders:", + "downloadAndSaveRecipe": "Download & Save Recipe", + "downloadMissingLoras": "Download Missing LoRAs", + "saveRecipe": "Save Recipe", + "loraCountInfo": "({existing}/{total} in library)", + "processingInput": "Processing input...", + "analyzingMetadata": "Analyzing image metadata...", + "downloadingLoras": "Downloading LoRAs...", + "savingRecipe": "Saving recipe...", + "startingDownload": "Starting download for LoRA {current}/{total}", + "deletedFromCivitai": "Deleted from Civitai", + "inLibrary": "In Library", + "notInLibrary": "Not in Library", + "earlyAccessRequired": "This LoRA requires early access payment to download.", + "earlyAccessEnds": "Early access ends on {date}.", + "earlyAccess": "Early Access", + "verifyEarlyAccess": "Verify that you have purchased early access before downloading.", + "duplicateRecipesFound": "{count} identical recipe(s) found in your library", + "duplicateRecipesDescription": "These recipes contain the same LoRAs with identical weights.", + "showDuplicates": "Show duplicates", + "hideDuplicates": "Hide duplicates", + "loraCount": "{count} LoRAs", + "recipePreviewAlt": "Recipe preview", + "loraPreviewAlt": "LoRA preview", + "errors": { + "selectImageFile": "Please select an image file", + "enterUrlOrPath": "Please enter a URL or file path", + "selectLoraRoot": "Please select a LoRA root directory" + } }, "refresh": { "title": "Refresh recipe list" @@ -832,7 +884,15 @@ "preparingForSharing": "Preparing recipe for sharing...", "downloadStarted": "Recipe download started", "shareError": "Error sharing recipe: {message}", - "sharePreparationError": "Error preparing recipe for sharing" + "sharePreparationError": "Error preparing recipe for sharing", + "selectImageFirst": "Please select an image first", + "enterRecipeName": "Please enter a recipe name", + "processingError": "Processing error: {message}", + "folderBrowserError": "Error loading folder browser: {message}", + "recipeSaveFailed": "Failed to save recipe: {error}", + "importFailed": "Import failed: {message}", + "folderTreeFailed": "Failed to load folder tree", + "folderTreeError": "Error loading folder tree" }, "models": { "noModelsSelected": "No models selected", @@ -890,15 +950,10 @@ "downloadError": "Download error: {message}" }, "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}", - "recipeSaveFailed": "Failed to save recipe: {error}", - "processingError": "Processing error: {message}", - "folderBrowserError": "Folder browser error: {message}" + "importFailed": "Failed to import example images: {message}" }, "triggerWords": { "loadFailed": "Could not load trained words", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 1e280693..4a256dd2 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -290,7 +290,59 @@ "controls": { "import": { "action": "导入配方", - "title": "从图片或 URL 导入配方" + "title": "从图片或 URL 导入配方", + "urlLocalPath": "URL / 本地路径", + "uploadImage": "上传图片", + "urlSectionDescription": "输入 Civitai 图片 URL 或本地文件路径以导入为配方。", + "imageUrlOrPath": "图片 URL 或文件路径:", + "urlPlaceholder": "https://civitai.com/images/... 或 C:/path/to/image.png", + "fetchImage": "获取图片", + "uploadSectionDescription": "上传带有 LoRA 元数据的图片以导入为配方。", + "selectImage": "选择图片", + "recipeName": "配方名称", + "recipeNamePlaceholder": "输入配方名称", + "tagsOptional": "标签(可选)", + "addTagPlaceholder": "添加标签", + "addTag": "添加", + "noTagsAdded": "未添加标签", + "lorasInRecipe": "此配方中的 LoRA", + "downloadLocationPreview": "下载位置预览:{path}", + "useDefaultPath": "使用默认路径", + "useDefaultPathTooltip": "启用后,文件将自动使用配置的路径模板进行组织", + "selectLoraRoot": "选择 LoRA 根目录", + "targetFolderPath": "目标文件夹路径:", + "folderPathPlaceholder": "输入文件夹路径或从下面的树中选择...", + "createNewFolder": "创建新文件夹", + "root": "根目录", + "browseFolders": "浏览文件夹:", + "downloadAndSaveRecipe": "下载并保存配方", + "downloadMissingLoras": "下载缺失的 LoRA", + "saveRecipe": "保存配方", + "loraCountInfo": "({existing}/{total} in library)", + "processingInput": "处理输入...", + "analyzingMetadata": "分析图像元数据...", + "downloadingLoras": "下载 LoRA...", + "savingRecipe": "保存配方...", + "startingDownload": "开始下载 LoRA {current}/{total}", + "deletedFromCivitai": "从 Civitai 中删除", + "inLibrary": "在库中", + "notInLibrary": "不在库中", + "earlyAccessRequired": "此 LoRA 需要提前访问权限才能下载。", + "earlyAccessEnds": "提前访问权限将于 {date} 结束。", + "earlyAccess": "提前访问", + "verifyEarlyAccess": "在下载之前,请验证您是否已购买提前访问权限。", + "duplicateRecipesFound": "在您的库中找到 {count} 个相同的配方。", + "duplicateRecipesDescription": "这些配方包含相同的 LoRA,权重完全相同。", + "showDuplicates": "显示重复项", + "hideDuplicates": "隐藏重复项", + "loraCount": "{count} LoRA", + "recipePreviewAlt": "配方预览", + "loraPreviewAlt": "LoRA 预览", + "errors": { + "selectImageFile": "请选择一个图像文件", + "enterUrlOrPath": "请输入 URL 或文件路径", + "selectLoraRoot": "请选择 LoRA 根目录" + } }, "refresh": { "title": "刷新配方列表" @@ -832,7 +884,15 @@ "preparingForSharing": "正在准备分享配方...", "downloadStarted": "配方下载已开始", "shareError": "分享配方出错:{message}", - "sharePreparationError": "准备分享配方出错" + "sharePreparationError": "准备分享配方出错", + "selectImageFirst": "请先选择图片", + "enterRecipeName": "请输入配方名称", + "processingError": "处理出错:{message}", + "folderBrowserError": "加载文件夹浏览器出错:{message}", + "recipeSaveFailed": "保存配方失败:{error}", + "importFailed": "导入失败:{message}", + "folderTreeFailed": "加载文件夹树失败", + "folderTreeError": "加载文件夹树出错" }, "models": { "noModelsSelected": "未选中模型", @@ -890,15 +950,10 @@ "downloadError": "下载错误:{message}" }, "import": { - "enterRecipeName": "请输入配方名称", - "selectImageFirst": "请先选择图片", "folderTreeFailed": "加载文件夹树失败", "folderTreeError": "加载文件夹树出错", "imagesImported": "示例图片导入成功", - "importFailed": "导入示例图片失败:{message}", - "recipeSaveFailed": "保存配方失败:{error}", - "processingError": "处理出错:{message}", - "folderBrowserError": "文件夹浏览器出错:{message}" + "importFailed": "导入示例图片失败:{message}" }, "triggerWords": { "loadFailed": "无法加载训练词", diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index c8649c8b..95d1549d 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -11,6 +11,7 @@ import { getModelApiClient } from '../api/modelApiFactory.js'; import { state } from '../state/index.js'; import { MODEL_TYPES } from '../api/apiConfig.js'; import { showToast } from '../utils/uiHelpers.js'; +import { translate } from '../utils/i18nHelpers.js'; export class ImportManager { constructor() { @@ -110,7 +111,7 @@ export class ImportManager { if (recipeName) recipeName.value = ''; const tagsContainer = document.getElementById('tagsContainer'); - if (tagsContainer) tagsContainer.innerHTML = '
'; + if (tagsContainer) tagsContainer.innerHTML = ``; // Clear folder path input const folderPathInput = document.getElementById('importFolderPath'); @@ -261,7 +262,7 @@ export class ImportManager { this.updateTargetPath(); } catch (error) { - showToast('toast.import.importFailed', { message: error.message }, 'error'); + showToast('toast.recipes.importFailed', { message: error.message }, 'error'); } } @@ -350,11 +351,11 @@ export class ImportManager { await this.folderTreeManager.loadTree(treeData.tree); } else { console.error('Failed to fetch folder tree:', treeData.error); - showToast('toast.import.folderTreeFailed', {}, 'error'); + showToast('toast.recipes.folderTreeFailed', {}, 'error'); } } catch (error) { console.error('Error initializing folder tree:', error); - showToast('toast.import.folderTreeError', {}, 'error'); + showToast('toast.recipes.folderTreeError', {}, 'error'); } } @@ -368,9 +369,7 @@ export class ImportManager { const pathDisplay = document.getElementById('importTargetPathDisplay'); const loraRoot = document.getElementById('importLoraRoot').value; - let fullPath = loraRoot || 'Select a LoRA root directory'; - - if (loraRoot) { + let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) { if (this.useDefaultPath) { // Show actual template path try { @@ -425,11 +424,11 @@ export class ImportManager { // Update the modal title const modalTitle = document.querySelector('#importModal h2'); - if (modalTitle) modalTitle.textContent = 'Download Missing LoRAs'; + if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs'); // Update the save button text const saveButton = document.querySelector('#locationStep .primary-btn'); - if (saveButton) saveButton.textContent = 'Download Missing LoRAs'; + if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs'); // Hide the back button const backButton = document.querySelector('#locationStep .secondary-btn'); diff --git a/static/js/managers/import/DownloadManager.js b/static/js/managers/import/DownloadManager.js index 4d9d6657..ffbefb20 100644 --- a/static/js/managers/import/DownloadManager.js +++ b/static/js/managers/import/DownloadManager.js @@ -1,4 +1,5 @@ import { showToast } from '../../utils/uiHelpers.js'; +import { translate } from '../../utils/i18nHelpers.js'; import { getModelApiClient } from '../../api/modelApiFactory.js'; import { MODEL_TYPES } from '../../api/apiConfig.js'; import { getStorageItem } from '../../utils/storageHelpers.js'; @@ -13,13 +14,13 @@ export class DownloadManager { const isDownloadOnly = !!this.importManager.recipeId; if (!isDownloadOnly && !this.importManager.recipeName) { - showToast('toast.import.enterRecipeName', {}, 'error'); + showToast('toast.recipes.enterRecipeName', {}, 'error'); return; } try { // Show progress indicator - this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? 'Downloading LoRAs...' : 'Saving recipe...'); + this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? translate('recipes.controls.import.downloadingLoras', {}, 'Downloading LoRAs...') : translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...')); // Only send the complete recipe to save if not in download-only mode if (!isDownloadOnly) { @@ -77,7 +78,7 @@ export class DownloadManager { if (!result.success) { // Handle save error console.error("Failed to save recipe:", result.error); - showToast('toast.import.recipeSaveFailed', { error: result.error }, 'error'); + showToast('toast.recipes.recipeSaveFailed', { error: result.error }, 'error'); // Close modal modalManager.closeModal('importModal'); return; @@ -107,7 +108,7 @@ export class DownloadManager { } catch (error) { console.error('Error:', error); - showToast('toast.import.processingError', { message: error.message }, 'error'); + showToast('toast.recipes.processingError', { message: error.message }, 'error'); } finally { this.importManager.loadingManager.hide(); } @@ -117,7 +118,7 @@ export class DownloadManager { // For download, we need to validate the target path const loraRoot = document.getElementById('importLoraRoot')?.value; if (!loraRoot) { - throw new Error('Please select a LoRA root directory'); + throw new Error(translate('recipes.controls.import.errors.selectLoraRoot', {}, 'Please select a LoRA root directory')); } // Build target path @@ -195,7 +196,7 @@ export class DownloadManager { currentLoraProgress = 0; // Initial status update for new LoRA - this.importManager.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.importManager.downloadableLoRAs.length}`); + this.importManager.loadingManager.setStatus(translate('recipes.controls.import.startingDownload', { current: i+1, total: this.importManager.downloadableLoRAs.length }, `Starting download for LoRA ${i+1}/${this.importManager.downloadableLoRAs.length}`)); updateProgress(0, completedDownloads, lora.name); try { diff --git a/static/js/managers/import/FolderBrowser.js b/static/js/managers/import/FolderBrowser.js index b314ef5e..32f39b80 100644 --- a/static/js/managers/import/FolderBrowser.js +++ b/static/js/managers/import/FolderBrowser.js @@ -1,4 +1,5 @@ import { showToast } from '../../utils/uiHelpers.js'; +import { translate } from '../../utils/i18nHelpers.js'; import { getStorageItem } from '../../utils/storageHelpers.js'; export class FolderBrowser { @@ -136,7 +137,7 @@ export class FolderBrowser { this.initializeFolderBrowser(); } catch (error) { console.error('Error in API calls:', error); - showToast('toast.import.folderBrowserError', { message: error.message }, 'error'); + showToast('toast.recipes.folderBrowserError', { message: error.message }, 'error'); } } @@ -204,7 +205,7 @@ export class FolderBrowser { const loraRoot = document.getElementById('importLoraRoot')?.value || ''; const newFolder = document.getElementById('importNewFolder')?.value?.trim() || ''; - let fullPath = loraRoot || 'Select a LoRA root directory'; + let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) { if (this.importManager.selectedFolder) { diff --git a/static/js/managers/import/ImageProcessor.js b/static/js/managers/import/ImageProcessor.js index 33f5d429..37f4b7ef 100644 --- a/static/js/managers/import/ImageProcessor.js +++ b/static/js/managers/import/ImageProcessor.js @@ -1,4 +1,5 @@ import { showToast } from '../../utils/uiHelpers.js'; +import { translate } from '../../utils/i18nHelpers.js'; export class ImageProcessor { constructor(importManager) { @@ -13,7 +14,7 @@ export class ImageProcessor { // Validate file type if (!file.type.match('image.*')) { - errorElement.textContent = 'Please select an image file'; + errorElement.textContent = translate('recipes.controls.import.errors.selectImageFile', {}, 'Please select an image file'); return; } @@ -32,7 +33,7 @@ export class ImageProcessor { // Validate input if (!input) { - errorElement.textContent = 'Please enter a URL or file path'; + errorElement.textContent = translate('recipes.controls.import.errors.enterUrlOrPath', {}, 'Please enter a URL or file path'); return; } @@ -40,7 +41,7 @@ export class ImageProcessor { errorElement.textContent = ''; // Show loading indicator - this.importManager.loadingManager.showSimpleLoading('Processing input...'); + this.importManager.loadingManager.showSimpleLoading(translate('recipes.controls.import.processingInput', {}, 'Processing input...')); try { // Check if it's a URL or a local file path @@ -156,12 +157,12 @@ export class ImageProcessor { async uploadAndAnalyzeImage() { if (!this.importManager.recipeImage) { - showToast('toast.import.selectImageFirst', {}, 'error'); + showToast('toast.recipes.selectImageFirst', {}, 'error'); return; } try { - this.importManager.loadingManager.showSimpleLoading('Analyzing image metadata...'); + this.importManager.loadingManager.showSimpleLoading(translate('recipes.controls.import.analyzingMetadata', {}, 'Analyzing image metadata...')); // Create form data for upload const formData = new FormData(); diff --git a/static/js/managers/import/RecipeDataManager.js b/static/js/managers/import/RecipeDataManager.js index 07543e5d..8f351859 100644 --- a/static/js/managers/import/RecipeDataManager.js +++ b/static/js/managers/import/RecipeDataManager.js @@ -1,4 +1,5 @@ import { showToast } from '../../utils/uiHelpers.js'; +import { translate } from '../../utils/i18nHelpers.js'; export class RecipeDataManager { constructor(importManager) { @@ -62,17 +63,17 @@ export class RecipeDataManager { // For file upload mode const reader = new FileReader(); reader.onload = (e) => { - imagePreview.innerHTML = `