diff --git a/.agents/logs/ai_changelog.md b/.agents/logs/ai_changelog.md index f0e7869..1fa92e4 100644 --- a/.agents/logs/ai_changelog.md +++ b/.agents/logs/ai_changelog.md @@ -1,5 +1,28 @@ # AI Changelog +## [Snapshot] 2026-08-06 - Migrate detail, doctor, assistant, picker, and notebook i18n + +**Implemented** + +- Migrated `web/modules/ui_detail.js`, `web/modules/ui_doctor.js`, and `web/modules/ui_notebooks.js` to the shared translator. +- Added locale entries for model editing, batch selection, Doctor diagnostics, Node Assistant actions, model replacement/insertion, history cards, and Notebook validation. +- Replaced language-condition branches with parameterized translation calls while keeping model names, paths, node types, scan filenames, and API messages dynamic. +- Preserved status styling for selected counts, empty/error states, healthy Doctor output, compatible-model messages, and missing-cover messages through DOM composition. +- Added the missing `galleryCancel` key used by the previous Gallery migration snapshot. + +**Validation** + +- JavaScript syntax checks passed for every file under `web/`. +- All 189 static translator calls in the migrated UI modules resolve in both `zh` and `en` dictionaries. +- Locale interpolation checks passed for Doctor scan status, picker selection, Assistant history counts, and Detail selection counts. +- `node tests/recipe_parser_roundtrip.mjs` passed. +- Python compilation passed for every `api/*.py` module and `scraper.py` using the bundled Python runtime. +- Targeted `git diff --check` passed; the existing user change in `README.md` was kept outside this snapshot. + +**Next** + +- Continue with the next Stage 2 group only after review; Sidebar, Recipe, and hash-resolver branches remain untouched. + ## [Snapshot] 2026-08-06 - Migrate grid and gallery i18n **Implemented** diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e2ad4da..c87dbd7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -56,6 +56,8 @@ Every i18n or licensing change is a local, reviewable snapshot. The implementati The second migration snapshot extends the shared translation contract to `ui_grid.js` and `ui_gallery.js`. Gallery confirmation and cover-selection banners are assembled with DOM nodes and `textContent` rather than translated raw HTML; user-controlled model names remain text content. These modules resolve strings at render time through `translate`, so existing global language changes continue to be reflected without introducing a new locale selector. +The third migration snapshot extends the same contract to `ui_detail.js`, `ui_doctor.js`, and `ui_notebooks.js`. Detail editing, model selection, Doctor diagnostics, the Node Assistant, the model picker, and Notebook validation now resolve user-facing strings through `translate` with parameter interpolation for counts, paths, model names, and scan status. Dynamic model/path/node data remains outside the locale dictionaries; rich UI state is composed with DOM nodes where translated text replaced previously formatted status HTML. + ## 2. Directory Structure (核心文件结构) ```text diff --git a/web/modules/locales.js b/web/modules/locales.js index 4820066..dc57682 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -96,6 +96,147 @@ export const i18n = { gallerySelectingCoverPrefix: '正在为模型 ', gallerySelectingCoverSuffix: ' 选择封面。请点击下方的图片。', galleryLoadImagesError: '加载历史图片失败', + galleryCancel: '取消', + detailBackDoctor: '⬅ 返回医生面板', + detailBackAssistant: '⬅ 返回助手面板', + detailJumpToBottom: '跳到底部', + detailEdit: '⚙️ 编辑', + detailNotesTitle: '📓 专属备注 (Notes)', + detailEditNotes: '编辑备注', + detailGeneratedGallery: '🖼️ 查看历史生成作品', + detailTrainedWords: '训练词:', + detailDescription: '描述:', + detailNotes: '备注:', + detailNoCompatibleModels: '没有找到兼容模型。', + detailCompatibleLoadFailed: '加载失败。', + detailToggleSound: '开启/关闭声音', + detailNoCover: '暂无封面', + detailPickGallery: '从历史图库挑选', + detailUploadLocal: '从本地上传', + detailUploadError: '错误: ', + detailUploadFailed: '上传失败: ', + detailModelInfo: '编辑模型信息', + detailFile: '文件:', + detailCustomNamePlaceholder: '自定义名称 (留空则使用原名)', + detailNotesPlaceholder: '📓 专属备注... (支持多行)', + detailPhysicalRename: '同步物理重命名', + detailPhysicalRenameDesc: '若不勾选则仅虚拟重命名,安全不破坏原文件。', + detailResetAll: '重置设置', + detailResetConfirm: '确定要重置该模型的所有自定义信息(名称、封面、备注)吗?', + detailResetCoverPreserved: '未找到可恢复的 C 站备份或原始封面。为避免丢失唯一图片,当前封面已保留;自定义名称和备注仍已重置。', + detailResetCoverFailed: '封面恢复未能完整完成,请检查文件权限或占用情况;自定义名称和备注仍已重置。', + detailResetFailed: '重置失败。', + detailCancel: '取消', + detailSaveChanges: '保存更改', + detailSaving: '保存中...', + detailSelectModels: '选择模型', + detailFolders: '目录列表', + detailSelectedItems: '已选 {count} 项', + detailSelectedItemsPrefix: '已选 ', + detailSelectedItemsSuffix: ' 项', + detailNothingSelected: '暂未选择', + detailLoadedModels: '已加载 {loaded} / {total} 个模型', + detailComputing: '计算中...', + detailSelectNoPreview: '选无预览图', + detailSelectNoDesc: '选无介绍', + detailSelectAll: '全选', + detailClear: '清空', + detailConfirm: '确定', + detailLoading: '加载中 (Loading)...', + detailAllLoaded: '✅ 已加载全部', + detailScrollMore: '滑动加载更多...', + detailNoModels: '没有找到模型。', + detailApiRestart: '❌ 请求后端 API 失败!\n\n你必须重启 ComfyUI 进程,刚写入后端的代码才能生效。', + doctorTitle: '全局体检中心', + doctorAutoScanOn: 'ComfyUI 启动时自动检测并替换缺失模型 [已开启]', + doctorAutoScanOff: 'ComfyUI 启动时自动检测并替换缺失模型 [默认关闭]', + doctorRefresh: '🔄 刷新缓存', + doctorRefreshTitle: '重新读取最新模型列表并清除报错', + doctorGraphUnavailable: '当前画布或节点不可用。', + doctorMissingChainInputs: '节点没有完整的 MODEL/CLIP 输入。', + doctorUnconnectedChainInputs: 'MODEL/CLIP 输入尚未全部连接。', + doctorMissingChainOutputs: '节点没有完整的 MODEL/CLIP 输出。', + doctorAmbiguousBranches: '检测到多个下游分支,请先整理或选择明确链路。', + doctorInvalidDownstream: '现有下游连线无效,无法安全改写。', + doctorUnsupportedInsertion: '当前节点不支持此插入方式。', + doctorCreateLoraFailed: '无法创建标准 LoRA Loader 节点。', + doctorLoraNotReady: 'LoRA 列表尚未就绪,请刷新 ComfyUI 模型列表后重试。', + assistantSelectNode: '请在画布中点击选中任意节点', + assistantNoModelParameter: '该节点没有受支持的模型参数', + assistantSelectedNode: '当前选中节点', + assistantQuickActions: '快捷操作', + assistantChangeWidget: '更换 {name}', + assistantChangeCurrent: '更换当前模型', + assistantVisualPicker: '{type} · 可视化选择', + assistantInsertBefore: '前方插入 LoRA', + assistantInsertAfter: '后方插入 LoRA', + assistantConnectInputs: '接入节点输入端', + assistantConnectOutputs: '接入节点输出端', + doctorTotal: '总模型', + doctorHealthy: '🟢 健康', + doctorMissing: '🔴 缺失', + doctorNoWorkflowModels: '当前工作流没有任何模型节点', + doctorAutoRedirected: '自动重定向', + doctorReady: '正常', + doctorMissingStatus: '丢失', + doctorCivitai: '🌐 C站', + doctorDeepHashScan: '🔍 深度哈希扫描', + doctorScanStarting: '⏳ 正在启动扫描引擎...', + doctorScanning: '⏳ 扫描中 ({current}/{total}) {filename}', + doctorScanError: '❌ 扫描过程中发生错误: ', + doctorMatching: '⏳ 正在匹配并替换飘红节点...', + doctorDeepScanSummary: '共深度扫描了 {total} 个缺失信息的模型。', + doctorNoLocalMatch: '❌ 扫描结束,本地未匹配到模型。\n\n{summary}\n\n这说明模型可能真的不在您的硬盘里,或者您删除了原本记录着哈希的源文件。\n请点击卡片上的【🌐 C站】去云端下载。', + doctorScanSuccess: '✅ 深度扫描成功!已自动修复该节点!\n\n{summary}', + doctorPollError: '❌ 状态轮询出错: ', + doctorScanFailed: '❌ 扫描出错: ', + doctorManualReplace: '🔀 手动替换', + doctorOriginalPath: '原路径: {path}', + assistantPreviewFailed: '无法加载预览', + assistantHistoryTitle: '🖼️ 历史生成图 ({count})', + assistantViewAll: '查看全部 →', + assistantRestoreWorkflow: '点击恢复此工作流', + assistantViewGallery: '点击查看完整图库', + pickerNoCompatible: '没有可供选择的兼容模型。', + pickerEyebrow: '节点助手 · 模型选择器', + pickerInsertBeforeTitle: '⬅ 在节点前方插入 LoRA', + pickerInsertAfterTitle: '在节点后方插入 LoRA ➡', + pickerChangeTitle: '🔀 更换当前模型', + pickerFolders: '📁 文件夹', + pickerSearch: '🔍 搜索名称或完整路径…', + pickerMainAll: '主模型:全部类型', + pickerNameAsc: '名称 A–Z', + pickerNameDesc: '名称 Z–A', + pickerByFolder: '按文件夹', + pickerLoadingCovers: '⏳ 正在加载模型封面…', + pickerCancel: '取消', + pickerInsertConfirm: '插入并自动连线', + pickerReplaceConfirm: '确认替换', + pickerCoversFailed: '⚠️ 封面加载失败,仍可按名称选择模型。', + doctorNodeLabel: '节点', + doctorAllHealthy: '🎉 太棒了!当前工作流中所有模型均在本地就绪。', + doctorLoadingPreview: '加载预览...', + doctorNoPreview: '暂无预览图', + doctorProfile: '📖 查看档案', + doctorBase: '基础模型', + doctorTriggerWords: '触发词', + doctorClickCopy: '点击复制', + doctorCopyAll: '📋 全部复制', + doctorNotes: '📝 我的备注', + pickerSelected: '已选择: {path}', + pickerChooseModel: '请选择一个模型。节点连接和强度不会被改动。', + pickerCompatibleMain: '✨ 已根据主模型 {name} 自动筛选兼容 LoRA;可在上方切换为全部类型。', + pickerCompatibleTag: '✨ 未找到已连接主模型,已根据当前 LoRA 的 {name} 标注筛选。', + pickerCompatibleNoTags: 'ℹ 已识别主模型 {name},但没有同类 LoRA 标注,因此暂时显示全部。', + pickerCompatibleUnknown: 'ℹ 未识别到主模型类型,暂时显示全部 LoRA;你仍可手动按基础模型筛选。', + pickerAllModels: '全部模型', + pickerResults: '{count} 个结果', + pickerCurrent: '当前', + pickerBaseModel: '基础模型', + pickerRoot: '根目录', + pickerNoMatches: '没有符合条件的模型。', + pickerOperationFailed: '操作失败:', + notebookSelectMain: '请先选择一个主模型。', closeHelp: '关闭说明', delSure: '⚠️ 确定吗?', dockTitle: '侧边停靠', @@ -446,6 +587,147 @@ export const i18n = { gallerySelectingCoverPrefix: 'Selecting cover for ', gallerySelectingCoverSuffix: '. Click an image below.', galleryLoadImagesError: 'Error loading images', + galleryCancel: 'Cancel', + detailBackDoctor: '⬅ Back to Doctor', + detailBackAssistant: '⬅ Back to Assistant', + detailJumpToBottom: 'Jump to bottom', + detailEdit: '⚙️ Edit', + detailNotesTitle: '📓 Custom Notes', + detailEditNotes: 'Edit Notes', + detailGeneratedGallery: '🖼️ View Generated Gallery', + detailTrainedWords: 'Trained Words:', + detailDescription: 'Description:', + detailNotes: 'Notes:', + detailNoCompatibleModels: 'No compatible models found.', + detailCompatibleLoadFailed: 'Failed to load.', + detailToggleSound: 'Toggle Sound', + detailNoCover: 'No Cover', + detailPickGallery: 'Pick from Gallery', + detailUploadLocal: 'Upload Local', + detailUploadError: 'Error: ', + detailUploadFailed: 'Upload failed: ', + detailModelInfo: 'Edit Model Info', + detailFile: 'File:', + detailCustomNamePlaceholder: 'Custom Name (Leave empty to use original)', + detailNotesPlaceholder: '📓 Custom Notes...', + detailPhysicalRename: 'Physical Rename on Disk', + detailPhysicalRenameDesc: 'Virtual rename only if unchecked. Safe for originals.', + detailResetAll: 'Reset All', + detailResetConfirm: 'Reset all custom info for this model?', + detailResetCoverPreserved: 'No recoverable Civitai backup or original cover was found. The current cover was kept to avoid losing the only image; the custom name and notes were still reset.', + detailResetCoverFailed: 'The cover restore could not be completed. Check file permissions or locks; the custom name and notes were still reset.', + detailResetFailed: 'Reset failed.', + detailCancel: 'Cancel', + detailSaveChanges: 'Save Changes', + detailSaving: 'Saving...', + detailSelectModels: 'Select Models', + detailFolders: 'Folders', + detailSelectedItems: 'Selected {count} items', + detailSelectedItemsPrefix: 'Selected ', + detailSelectedItemsSuffix: ' items', + detailNothingSelected: 'Nothing selected', + detailLoadedModels: 'Loaded {loaded} / {total} models', + detailComputing: 'Computing...', + detailSelectNoPreview: 'Select No Preview', + detailSelectNoDesc: 'Select No Desc', + detailSelectAll: 'Select All', + detailClear: 'Clear', + detailConfirm: 'Confirm', + detailLoading: 'Loading...', + detailAllLoaded: '✅ All models loaded', + detailScrollMore: 'Scroll for more...', + detailNoModels: 'No models found.', + detailApiRestart: '❌ Backend API request failed!\n\nYou must restart the ComfyUI process for the newly written backend code to take effect.', + doctorTitle: 'Global Health Center', + doctorAutoScanOn: 'Auto-Detect on ComfyUI Startup [ON]', + doctorAutoScanOff: 'Auto-Detect on ComfyUI Startup [OFF]', + doctorRefresh: '🔄 Refresh', + doctorRefreshTitle: 'Reload model list and clear errors', + doctorGraphUnavailable: 'The current graph or node is unavailable.', + doctorMissingChainInputs: 'The node does not expose both MODEL and CLIP inputs.', + doctorUnconnectedChainInputs: 'Both MODEL and CLIP inputs must already be connected.', + doctorMissingChainOutputs: 'The node does not expose both MODEL and CLIP outputs.', + doctorAmbiguousBranches: 'Multiple downstream branches were detected; choose an explicit chain first.', + doctorInvalidDownstream: 'An existing downstream link is invalid and cannot be safely changed.', + doctorUnsupportedInsertion: 'This insertion is not available for the selected node.', + doctorCreateLoraFailed: 'Unable to create a standard LoRA Loader node.', + doctorLoraNotReady: 'The LoRA list is not ready. Refresh ComfyUI model lists and try again.', + assistantSelectNode: 'select any node in the canvas', + assistantNoModelParameter: 'No supported model parameter on this node', + assistantSelectedNode: 'Selected node', + assistantQuickActions: 'Quick actions', + assistantChangeWidget: 'Change {name}', + assistantChangeCurrent: 'Change current model', + assistantVisualPicker: '{type} · Visual picker', + assistantInsertBefore: 'Insert LoRA before', + assistantInsertAfter: 'Insert LoRA after', + assistantConnectInputs: 'Connect to node inputs', + assistantConnectOutputs: 'Connect to node outputs', + doctorTotal: 'Total', + doctorHealthy: '🟢 Healthy', + doctorMissing: '🔴 Missing', + doctorNoWorkflowModels: 'No models found in this workflow', + doctorAutoRedirected: 'Auto-Redirected', + doctorReady: 'Ready', + doctorMissingStatus: 'Missing', + doctorCivitai: '🌐 Civitai', + doctorDeepHashScan: '🔍 Deep Hash Scan', + doctorScanStarting: '⏳ Starting scan engine...', + doctorScanning: '⏳ Scanning ({current}/{total}) {filename}', + doctorScanError: '❌ Scan error: ', + doctorMatching: '⏳ Matching and resolving red nodes...', + doctorDeepScanSummary: 'Deep scanned {total} models with missing info.', + doctorNoLocalMatch: '❌ Scan finished, but no local match found.\n\n{summary}\n\nThis means the model is truly missing from your disk, or the original source file with hash was deleted.\nPlease click [🌐 Civitai] to download it from the cloud.', + doctorScanSuccess: '✅ Deep Scan successful! Node auto-healed!\n\n{summary}', + doctorPollError: '❌ Poll Error: ', + doctorScanFailed: '❌ Scan Error: ', + doctorManualReplace: '🔀 Manual Replace', + doctorOriginalPath: 'Original: {path}', + assistantPreviewFailed: 'Failed to load preview', + assistantHistoryTitle: '🖼️ History ({count})', + assistantViewAll: 'View All →', + assistantRestoreWorkflow: 'Click to restore workflow', + assistantViewGallery: 'Click to view full gallery', + pickerNoCompatible: 'No compatible models are available.', + pickerEyebrow: 'Node Assistant · Model Picker', + pickerInsertBeforeTitle: '⬅ Insert LoRA Before Node', + pickerInsertAfterTitle: 'Insert LoRA After Node ➡', + pickerChangeTitle: '🔀 Change Current Model', + pickerFolders: '📁 Folders', + pickerSearch: '🔍 Search name or full path…', + pickerMainAll: 'Main model: All types', + pickerNameAsc: 'Name A–Z', + pickerNameDesc: 'Name Z–A', + pickerByFolder: 'By Folder', + pickerLoadingCovers: '⏳ Loading model covers…', + pickerCancel: 'Cancel', + pickerInsertConfirm: 'Insert & Auto-Connect', + pickerReplaceConfirm: 'Confirm Replacement', + pickerCoversFailed: '⚠️ Covers failed to load; models remain selectable by name.', + doctorNodeLabel: 'Node', + doctorAllHealthy: '🎉 Awesome! All models in this workflow are healthy.', + doctorLoadingPreview: 'Loading preview...', + doctorNoPreview: 'No preview', + doctorProfile: '📖 View Profile', + doctorBase: 'Base', + doctorTriggerWords: 'Trigger Words', + doctorClickCopy: 'Click to copy', + doctorCopyAll: '📋 Copy All', + doctorNotes: '📝 My Notes', + pickerSelected: 'Selected: {path}', + pickerChooseModel: 'Choose a model. Existing connections and strengths will be preserved.', + pickerCompatibleMain: '✨ Compatible LoRAs are filtered for the main model {name}. Use the filter above to show all types.', + pickerCompatibleTag: '✨ No connected main model was identified, so the current LoRA tag {name} is used.', + pickerCompatibleNoTags: 'ℹ Main model {name} was identified, but no matching LoRA tags were found, so all models remain visible.', + pickerCompatibleUnknown: 'ℹ The main model type could not be identified. All LoRAs remain visible and can be filtered manually.', + pickerAllModels: 'All Models', + pickerResults: '{count} results', + pickerCurrent: 'Current', + pickerBaseModel: 'Base model', + pickerRoot: 'Root', + pickerNoMatches: 'No models match the current filters.', + pickerOperationFailed: 'Operation failed: ', + notebookSelectMain: 'Please select a Main Model first.', closeHelp: 'Close Manual', delSure: '⚠️ Sure?', dockTitle: 'Dock to Left', diff --git a/web/modules/ui_detail.js b/web/modules/ui_detail.js index 116176b..0b96cfd 100644 --- a/web/modules/ui_detail.js +++ b/web/modules/ui_detail.js @@ -4,14 +4,10 @@ */ import { app } from "../../../scripts/app.js"; -import { i18n } from './locales.js'; +import { translate } from './locales.js'; import { escapeHtml, setSafeRichHtml } from './safe_dom.js'; -const t = (key) => { - let lang = window.anomalous_browser_lang || 'zh'; - if (lang.startsWith('en')) lang = 'en'; - return (i18n[lang] && i18n[lang][key]) ? i18n[lang][key] : (i18n['zh'][key] || key); -}; +const t = (key, params) => translate(key, params); @@ -84,11 +80,11 @@ export function showDetail(model) { } if (isFromDoctor) { - backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回医生面板' : '⬅ Back to Doctor'; + backBtn.textContent = t('detailBackDoctor'); backBtn.style.background = '#8AB4F8'; backBtn.style.color = '#000'; } else if (isFromAssistant) { - backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回助手面板' : '⬅ Back to Assistant'; + backBtn.textContent = t('detailBackAssistant'); backBtn.style.background = '#8AB4F8'; backBtn.style.color = '#000'; } else { @@ -204,7 +200,7 @@ export function showDetail(model) { const jumpBtn = document.createElement('button'); jumpBtn.innerHTML = '⬇️'; - jumpBtn.title = t('jumpToBottom') || 'Jump to bottom'; + jumpBtn.title = t('detailJumpToBottom'); jumpBtn.style.padding = '6px 12px'; jumpBtn.style.background = '#444'; jumpBtn.style.color = '#fff'; @@ -224,7 +220,7 @@ export function showDetail(model) { header.appendChild(jumpBtn); const applyDetailBtn = document.createElement('button'); - applyDetailBtn.innerHTML = t('applyToCanvas') || 'Apply to Canvas'; + applyDetailBtn.textContent = t('applyToCanvas'); applyDetailBtn.style.padding = '6px 12px'; applyDetailBtn.style.background = '#007bff'; applyDetailBtn.style.color = '#fff'; @@ -371,7 +367,7 @@ export function showDetail(model) { } const editMetaBtn = document.createElement('button'); - editMetaBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⚙️ 编辑' : '⚙️ Edit'; + editMetaBtn.textContent = t('detailEdit'); editMetaBtn.style.marginLeft = m.civitai_url ? '10px' : 'auto'; editMetaBtn.style.padding = '4px 8px'; editMetaBtn.style.background = '#444'; @@ -413,7 +409,7 @@ export function showDetail(model) { notesHeader.style.marginBottom = '8px'; const notesTitle = document.createElement('div'); - notesTitle.innerHTML = window.anomalous_browser_lang === 'zh' ? '📓 专属备注 (Notes)' : '📓 Custom Notes'; + notesTitle.textContent = t('detailNotesTitle'); notesTitle.style.color = '#a38d53'; notesTitle.style.fontWeight = '600'; notesTitle.style.fontSize = '0.85em'; @@ -421,7 +417,7 @@ export function showDetail(model) { const notesEditBtn = document.createElement('button'); notesEditBtn.innerHTML = '✏️'; - notesEditBtn.title = window.anomalous_browser_lang === 'zh' ? '编辑备注' : 'Edit Notes'; + notesEditBtn.title = t('detailEditNotes'); notesEditBtn.style.background = 'transparent'; notesEditBtn.style.border = 'none'; notesEditBtn.style.color = '#a38d53'; @@ -472,7 +468,7 @@ export function showDetail(model) { galleryBtn.style.padding = '10px'; galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.border = '1px solid #3c4043'; - galleryBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🖼️ 查看历史生成作品' : '🖼️ View Generated Gallery'; + galleryBtn.textContent = t('detailGeneratedGallery'); galleryBtn.onmouseover = () => { galleryBtn.style.background = '#3c4043'; galleryBtn.style.borderColor = '#8ab4f8'; }; galleryBtn.onmouseout = () => { galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.borderColor = '#3c4043'; }; @@ -494,7 +490,7 @@ export function showDetail(model) { twHeader.style.marginBottom = '5px'; const twLabel = document.createElement('strong'); - twLabel.innerText = 'Trained Words:'; + twLabel.textContent = t('detailTrainedWords'); twHeader.appendChild(twLabel); const copyAll = document.createElement('button'); @@ -554,7 +550,7 @@ export function showDetail(model) { // important for flex scroll const descLabel = document.createElement('strong'); - descLabel.innerText = 'Description:'; + descLabel.textContent = t('detailDescription'); descLabel.style.marginBottom = '5px'; descCont.appendChild(descLabel); @@ -580,7 +576,7 @@ export function showDetail(model) { notesCont.style.marginTop = '10px'; const notesLabel = document.createElement('strong'); - notesLabel.innerText = 'Notes:'; + notesLabel.textContent = t('detailNotes'); notesCont.appendChild(notesLabel); const notesText = document.createElement('div'); @@ -622,7 +618,10 @@ export function showDetail(model) { window.anomalous_update_hash_cache(d.models); } if (!d.models || d.models.length === 0) { - compList.innerHTML = `No compatible models found.`; + const noCompatible = document.createElement('span'); + noCompatible.style.color = '#888'; + noCompatible.textContent = t('detailNoCompatibleModels'); + compList.replaceChildren(noCompatible); return; } d.models.forEach(m_comp => { @@ -667,7 +666,10 @@ export function showDetail(model) { }); }) .catch(e => { - compList.innerHTML = `Failed to load.`; + const compatibleError = document.createElement('span'); + compatibleError.style.color = '#ff4444'; + compatibleError.textContent = t('detailCompatibleLoadFailed'); + compList.replaceChildren(compatibleError); }); } // --------------------------------- @@ -755,7 +757,7 @@ export function showEditModal(model) { muteBtn.style.cursor = 'pointer'; muteBtn.style.fontSize = '14px'; muteBtn.style.zIndex = '10'; - muteBtn.title = window.anomalous_browser_lang === 'zh' ? '开启/关闭声音' : 'Toggle Sound'; + muteBtn.title = t('detailToggleSound'); muteBtn.onclick = (e) => { e.stopPropagation(); video.muted = !video.muted; @@ -773,7 +775,12 @@ export function showEditModal(model) { previewContainer.appendChild(img); } } else { - previewContainer.innerHTML = `
${window.anomalous_browser_lang === 'zh' ? '暂无封面' : 'No Cover'}
`; + const noCover = document.createElement('div'); + noCover.style.color = '#9aa0a6'; + noCover.style.fontSize = '0.9em'; + noCover.style.textAlign = 'center'; + noCover.textContent = t('detailNoCover'); + previewContainer.appendChild(noCover); } const coverRow = document.createElement('div'); @@ -782,7 +789,7 @@ export function showEditModal(model) { coverRow.style.gap = '8px'; const galleryBtn = document.createElement('button'); - galleryBtn.innerHTML = '🖼️ ' + (window.anomalous_browser_lang === 'zh' ? '从历史图库挑选' : 'Pick from Gallery'); + galleryBtn.textContent = `🖼️ ${t('detailPickGallery')}`; galleryBtn.style.padding = '8px'; galleryBtn.style.background = '#303134'; galleryBtn.style.color = '#8ab4f8'; @@ -799,7 +806,7 @@ export function showEditModal(model) { }; const localBtn = document.createElement('button'); - localBtn.innerHTML = '📁 ' + (window.anomalous_browser_lang === 'zh' ? '从本地上传' : 'Upload Local'); + localBtn.textContent = `📁 ${t('detailUploadLocal')}`; localBtn.style.padding = '8px'; localBtn.style.background = '#303134'; localBtn.style.color = '#8ab4f8'; @@ -834,10 +841,10 @@ export function showEditModal(model) { } document.body.removeChild(modal); } else { - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); + alert(t('detailUploadError') + data.message); } } catch (err) { - alert((window.anomalous_browser_lang === 'zh' ? '上传失败: ' : 'Upload failed: ') + err); + alert(t('detailUploadFailed') + err); } } }; @@ -858,14 +865,17 @@ export function showEditModal(model) { rightCol.style.gap = '15px'; const title = document.createElement('h2'); - title.innerText = window.anomalous_browser_lang === 'zh' ? '编辑模型信息' : 'Edit Model Info'; + title.textContent = t('detailModelInfo'); title.style.margin = '0'; title.style.color = '#e8eaed'; title.style.fontSize = '1.25em'; title.style.fontWeight = '500'; const filenameLabel = document.createElement('div'); - filenameLabel.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '文件:' : 'File:'} ${escapeHtml(model.filename)}`; + const filenamePrefix = document.createElement('span'); + filenamePrefix.style.color = '#9aa0a6'; + filenamePrefix.textContent = t('detailFile'); + filenameLabel.append(filenamePrefix, document.createTextNode(` ${model.filename}`)); filenameLabel.style.color = '#e8eaed'; filenameLabel.style.fontSize = '0.9em'; filenameLabel.style.wordBreak = 'break-all'; @@ -884,14 +894,14 @@ export function showEditModal(model) { `; const nameInput = document.createElement('input'); - nameInput.placeholder = window.anomalous_browser_lang === 'zh' ? '自定义名称 (留空则使用原名)' : 'Custom Name (Leave empty to use original)'; + nameInput.placeholder = t('detailCustomNamePlaceholder'); nameInput.value = (model.metadata && model.metadata.custom_name) ? model.metadata.custom_name : ''; nameInput.style.cssText = inputStyle; nameInput.onfocus = () => nameInput.style.borderColor = '#8ab4f8'; nameInput.onblur = () => nameInput.style.borderColor = '#5f6368'; const notesInput = document.createElement('textarea'); - notesInput.placeholder = window.anomalous_browser_lang === 'zh' ? '📓 专属备注... (支持多行)' : '📓 Custom Notes...'; + notesInput.placeholder = t('detailNotesPlaceholder'); notesInput.value = (model.metadata && model.metadata.custom_notes) ? model.metadata.custom_notes : ''; notesInput.style.cssText = inputStyle; notesInput.style.flex = '1'; // fill remaining space @@ -935,7 +945,7 @@ export function showEditModal(model) { const physicalLabel = document.createElement('label'); physicalLabel.htmlFor = 'anomalous-physical-rename-checkbox'; - physicalLabel.innerText = window.anomalous_browser_lang === 'zh' ? '同步物理重命名' : 'Physical Rename on Disk'; + physicalLabel.textContent = t('detailPhysicalRename'); physicalLabel.style.color = '#e8eaed'; physicalLabel.style.fontSize = '0.9em'; physicalLabel.style.cursor = 'pointer'; @@ -947,7 +957,7 @@ export function showEditModal(model) { physicalDesc.style.fontSize = '0.8em'; physicalDesc.style.color = '#9aa0a6'; physicalDesc.style.marginLeft = '22px'; - physicalDesc.innerText = window.anomalous_browser_lang === 'zh' ? '若不勾选则仅虚拟重命名,安全不破坏原文件。' : 'Virtual rename only if unchecked. Safe for originals.'; + physicalDesc.textContent = t('detailPhysicalRenameDesc'); physicalRow.appendChild(physicalCheckboxWrapper); physicalRow.appendChild(physicalDesc); @@ -962,7 +972,7 @@ export function showEditModal(model) { leftActions.style.gap = '10px'; const resetBtn = document.createElement('button'); - resetBtn.innerText = window.anomalous_browser_lang === 'zh' ? '重置设置' : 'Reset All'; + resetBtn.textContent = t('detailResetAll'); resetBtn.style.padding = '8px 16px'; resetBtn.style.background = 'transparent'; resetBtn.style.color = '#f28b82'; @@ -971,7 +981,7 @@ export function showEditModal(model) { resetBtn.style.cursor = 'pointer'; resetBtn.style.fontWeight = '500'; resetBtn.onclick = async () => { - if (!confirm(window.anomalous_browser_lang === 'zh' ? '确定要重置该模型的所有自定义信息(名称、封面、备注)吗?' : 'Reset all custom info for this model?')) return; + if (!confirm(t('detailResetConfirm'))) return; document.body.removeChild(modal); try { const res = await fetch('/anomalous/update_metadata', { @@ -1002,22 +1012,16 @@ export function showEditModal(model) { } if (result.cover_reset === false) { const noSource = result.cover_reset_source === 'preserved_current'; - alert(window.anomalous_browser_lang === 'zh' - ? (noSource - ? '未找到可恢复的 C 站备份或原始封面。为避免丢失唯一图片,当前封面已保留;自定义名称和备注仍已重置。' - : '封面恢复未能完整完成,请检查文件权限或占用情况;自定义名称和备注仍已重置。') - : (noSource - ? 'No recoverable Civitai backup or original cover was found. The current cover was kept to avoid losing the only image; the custom name and notes were still reset.' - : 'The cover restore could not be completed. Check file permissions or locks; the custom name and notes were still reset.')); + alert(t(noSource ? 'detailResetCoverPreserved' : 'detailResetCoverFailed')); } } else { - alert(result.message || (window.anomalous_browser_lang === 'zh' ? '重置失败。' : 'Reset failed.')); + alert(result.message || t('detailResetFailed')); } } catch (e) { console.error(e); } }; const cancelBtn = document.createElement('button'); - cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel'; + cancelBtn.textContent = t('detailCancel'); cancelBtn.style.padding = '8px 16px'; cancelBtn.style.background = 'transparent'; cancelBtn.style.color = '#8ab4f8'; @@ -1031,7 +1035,7 @@ export function showEditModal(model) { leftActions.appendChild(cancelBtn); const saveBtn = document.createElement('button'); - saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存更改' : 'Save Changes'; + saveBtn.textContent = t('detailSaveChanges'); saveBtn.style.padding = '8px 24px'; saveBtn.style.background = '#8ab4f8'; saveBtn.style.color = '#202124'; @@ -1042,7 +1046,7 @@ export function showEditModal(model) { saveBtn.onclick = () => { const newName = nameInput.value.trim(); const newNotes = notesInput.value.trim(); - saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存中...' : 'Saving...'; + saveBtn.textContent = t('detailSaving'); saveBtn.disabled = true; fetch('/anomalous/update_metadata', { @@ -1071,11 +1075,11 @@ export function showEditModal(model) { this.showDetail(model); } } else { - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); + alert(t('detailUploadError') + data.message); } }).catch(e => { document.body.removeChild(modal); - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + e); + alert(t('detailUploadError') + e); }); }; @@ -1150,7 +1154,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { titleContainer.style.gap = '12px'; const title = document.createElement('h2'); - title.innerText = window.anomalous_browser_lang === 'zh' ? '选择模型' : 'Select Models'; + title.textContent = t('detailSelectModels'); title.style.margin = '0'; title.style.color = '#e8eaed'; title.style.fontSize = '20px'; @@ -1186,7 +1190,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { leftPanel.style.background = '#171717'; const leftTitle = document.createElement('div'); - leftTitle.innerText = window.anomalous_browser_lang === 'zh' ? '目录列表' : 'Folders'; + leftTitle.textContent = t('detailFolders'); leftTitle.style.padding = '16px 24px'; leftTitle.style.fontWeight = '500'; leftTitle.style.color = '#9aa0a6'; @@ -1250,11 +1254,18 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { for (const set of localSelection.values()) total += set.size; if (total > 0) { - summaryText.innerHTML = window.anomalous_browser_lang === 'zh' - ? `已选 ${total} 项` - : `Selected ${total} items`; + const countLabel = document.createElement('span'); + countLabel.textContent = total; + countLabel.style.color = '#8ab4f8'; + countLabel.style.fontSize = '16px'; + countLabel.style.fontWeight = '500'; + summaryText.replaceChildren( + document.createTextNode(t('detailSelectedItemsPrefix')), + countLabel, + document.createTextNode(t('detailSelectedItemsSuffix')), + ); } else { - summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? '暂未选择' : 'Nothing selected'; + summaryText.textContent = t('detailNothingSelected'); } treeContainer.querySelectorAll('.folder-item').forEach(fBtn => { @@ -1274,9 +1285,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { // Also update total models text in grid const statusDiv = grid.querySelector('.status-text'); if (statusDiv && !isLoading) { - statusDiv.innerText = window.anomalous_browser_lang === 'zh' - ? `已加载 ${currentModels.length} / ${currentTotal} 个模型` - : `Loaded ${currentModels.length} / ${currentTotal} models`; + statusDiv.textContent = t('detailLoadedModels', { loaded: currentModels.length, total: currentTotal }); } }; @@ -1353,7 +1362,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { return; } - summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? '计算中...' : 'Computing...'; + summaryText.textContent = t('detailComputing'); try { const res = await fetch(`/anomalous/batch_select?folderKey=${encodeURIComponent(currentFolderKey)}&action=${action}`); if (!res.ok) throw new Error('API failed'); @@ -1373,20 +1382,20 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { updateGridCheckboxes(); } catch (e) { console.error(e); - summaryText.innerHTML = 'Error'; + summaryText.textContent = t('detailUploadError') + e.message; setTimeout(() => updateGridCheckboxes(), 2000); } }; - toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '选无预览图' : 'Select No Preview', () => handleBatchSelect('no_preview'), true)); - toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '选无介绍' : 'Select No Desc', () => handleBatchSelect('no_desc'), true)); + toolbar.appendChild(createTBtn(t('detailSelectNoPreview'), () => handleBatchSelect('no_preview'), true)); + toolbar.appendChild(createTBtn(t('detailSelectNoDesc'), () => handleBatchSelect('no_desc'), true)); const divi = document.createElement('div'); divi.style.width = '1px'; divi.style.height = '24px'; divi.style.background = '#3c4043'; divi.style.margin = '0 8px'; toolbar.appendChild(divi); - toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '全选' : 'Select All', () => handleBatchSelect('all'))); - toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '清空' : 'Clear', () => handleBatchSelect('none'))); + toolbar.appendChild(createTBtn(t('detailSelectAll'), () => handleBatchSelect('all'))); + toolbar.appendChild(createTBtn(t('detailClear'), () => handleBatchSelect('none'))); const summaryText = document.createElement('div'); summaryText.style.marginLeft = 'auto'; @@ -1421,7 +1430,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { footer.style.gap = '12px'; const cancelBtn = document.createElement('button'); - cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel'; + cancelBtn.textContent = t('detailCancel'); cancelBtn.style.padding = '8px 24px'; cancelBtn.style.background = 'transparent'; cancelBtn.style.color = '#8ab4f8'; @@ -1435,7 +1444,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { cancelBtn.onclick = () => document.body.removeChild(modal); const confirmBtn = document.createElement('button'); - confirmBtn.innerText = window.anomalous_browser_lang === 'zh' ? '确定' : 'Confirm'; + confirmBtn.textContent = t('detailConfirm'); confirmBtn.style.padding = '8px 24px'; confirmBtn.style.background = '#8ab4f8'; confirmBtn.style.color = '#202124'; // dark text on bright accent button @@ -1632,7 +1641,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { loadingIndicator.style.textAlign = 'center'; loadingIndicator.style.color = '#9aa0a6'; loadingIndicator.style.fontSize = '16px'; - loadingIndicator.innerText = window.anomalous_browser_lang === 'zh' ? '加载中 (Loading)...' : 'Loading...'; + loadingIndicator.textContent = t('detailLoading'); grid.appendChild(loadingIndicator); try { @@ -1640,7 +1649,16 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { const url = `${currentBaseUrl}${sep}page=${currentPage}&limit=50`; const res = await fetch(url); if (!res.ok) { - grid.innerHTML = `
❌ 请求后端 API 失败!

你必须重启 ComfyUI 进程,刚写入后端的代码才能生效。
`; + const apiError = document.createElement('div'); + apiError.className = 'status-text'; + apiError.style.gridColumn = '1 / -1'; + apiError.style.padding = '60px'; + apiError.style.textAlign = 'center'; + apiError.style.color = '#f28b82'; + apiError.style.fontSize = '16px'; + apiError.style.whiteSpace = 'pre-line'; + apiError.textContent = t('detailApiRestart'); + grid.replaceChildren(apiError); isLoading = false; return; } @@ -1662,7 +1680,15 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { grid.removeChild(loadingIndicator); if (currentModels.length === 0 && !isLoadMore) { - grid.innerHTML = `
No models found.
`; + const emptyModels = document.createElement('div'); + emptyModels.className = 'status-text'; + emptyModels.style.gridColumn = '1 / -1'; + emptyModels.style.padding = '60px'; + emptyModels.style.textAlign = 'center'; + emptyModels.style.color = '#9aa0a6'; + emptyModels.style.fontSize = '16px'; + emptyModels.textContent = t('detailNoModels'); + grid.replaceChildren(emptyModels); } else { grid.appendChild(renderCards(fetchedModels)); updateGridCheckboxes(); @@ -1676,9 +1702,9 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { statusText.style.color = '#5f6368'; statusText.style.fontSize = '14px'; if (currentModels.length >= currentTotal) { - statusText.innerText = window.anomalous_browser_lang === 'zh' ? '✅ 已加载全部' : '✅ All models loaded'; + statusText.textContent = t('detailAllLoaded'); } else { - statusText.innerText = window.anomalous_browser_lang === 'zh' ? '滑动加载更多...' : 'Scroll for more...'; + statusText.textContent = t('detailScrollMore'); } grid.appendChild(statusText); } @@ -1716,7 +1742,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { allBtn.style.display = 'flex'; allBtn.style.justifyContent = 'space-between'; allBtn.style.alignItems = 'center'; - allBtn.innerHTML = `🌟 ${window.anomalous_browser_lang === 'zh' ? '全部模型' : 'All Models'}`; + allBtn.textContent = `🌟 ${t('models')}`; let loadFolder = async (fBtn, folderKey, fetchUrl) => { treeContainer.querySelectorAll('.folder-item').forEach(d => { diff --git a/web/modules/ui_doctor.js b/web/modules/ui_doctor.js index 05fd796..1cf8b27 100644 --- a/web/modules/ui_doctor.js +++ b/web/modules/ui_doctor.js @@ -1,4 +1,5 @@ import { app } from "../../../scripts/app.js"; +import { translate } from './locales.js'; import { escapeHtml } from './safe_dom.js'; import { analyzeModelChainInsertion, @@ -16,6 +17,8 @@ import { * Extracted Doctor Panel & Assistant Panel methods. */ +const t = (key, params) => translate(key, params); + export function initDoctorPanel() { this.doctorPanelInitialized = true; this.doctorPanel.innerHTML = ''; @@ -32,7 +35,7 @@ export function initDoctorPanel() { const titleEl = document.createElement('div'); titleEl.style.cssText = 'display:flex;align-items:center;gap:12px;'; - titleEl.innerHTML = `🩺${window.anomalous_browser_lang === 'zh' ? '全局体检中心' : 'Global Health Center'}`; + titleEl.innerHTML = `🩺${t('doctorTitle')}`; // Header control group on the right side (contains toggle & close button) const controlGroup = document.createElement('div'); @@ -44,13 +47,9 @@ export function initDoctorPanel() { const renderAutoScanToggle = () => { let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true'; - autoScanToggle.innerHTML = window.anomalous_browser_lang === 'zh' - ? (isAutoEnabled - ? '🛎️ComfyUI 启动时自动检测并替换缺失模型 [已开启]' - : '🔕ComfyUI 启动时自动检测并替换缺失模型 [默认关闭]') - : (isAutoEnabled - ? '🛎️Auto-Detect on ComfyUI Startup [ON]' - : '🔕Auto-Detect on ComfyUI Startup [OFF]'); + autoScanToggle.innerHTML = isAutoEnabled + ? `🛎️${t('doctorAutoScanOn')}` + : `🔕${t('doctorAutoScanOff')}`; }; renderAutoScanToggle(); @@ -63,9 +62,9 @@ export function initDoctorPanel() { }; const refreshBtn = document.createElement('button'); - refreshBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔄 刷新缓存' : '🔄 Refresh'; + refreshBtn.textContent = t('doctorRefresh'); refreshBtn.style.cssText = 'background:rgba(138,180,248,0.1);border:1px solid rgba(138,180,248,0.3);color:#8AB4F8;font-size:12px;cursor:pointer;padding:6px 12px;border-radius:6px;transition:all 0.2s; font-weight:600;'; - refreshBtn.title = window.anomalous_browser_lang === 'zh' ? '重新读取最新模型列表并清除报错' : 'Reload model list and clear errors'; + refreshBtn.title = t('doctorRefreshTitle'); refreshBtn.onmouseover = () => { refreshBtn.style.background = 'rgba(138,180,248,0.2)'; }; refreshBtn.onmouseout = () => { refreshBtn.style.background = 'rgba(138,180,248,0.1)'; }; refreshBtn.onclick = async () => { @@ -119,16 +118,15 @@ export function initDoctorPanel() { } function getInsertionCapabilityMessage(capability) { - const zh = window.anomalous_browser_lang === 'zh'; const messages = { - missing_graph_or_node: zh ? '当前画布或节点不可用。' : 'The current graph or node is unavailable.', - missing_chain_inputs: zh ? '节点没有完整的 MODEL/CLIP 输入。' : 'The node does not expose both MODEL and CLIP inputs.', - unconnected_chain_inputs: zh ? 'MODEL/CLIP 输入尚未全部连接。' : 'Both MODEL and CLIP inputs must already be connected.', - missing_chain_outputs: zh ? '节点没有完整的 MODEL/CLIP 输出。' : 'The node does not expose both MODEL and CLIP outputs.', - ambiguous_downstream_branches: zh ? '检测到多个下游分支,请先整理或选择明确链路。' : 'Multiple downstream branches were detected; choose an explicit chain first.', - invalid_downstream_link: zh ? '现有下游连线无效,无法安全改写。' : 'An existing downstream link is invalid and cannot be safely changed.', + missing_graph_or_node: 'doctorGraphUnavailable', + missing_chain_inputs: 'doctorMissingChainInputs', + unconnected_chain_inputs: 'doctorUnconnectedChainInputs', + missing_chain_outputs: 'doctorMissingChainOutputs', + ambiguous_downstream_branches: 'doctorAmbiguousBranches', + invalid_downstream_link: 'doctorInvalidDownstream', }; - return messages[capability?.code] || (zh ? '当前节点不支持此插入方式。' : 'This insertion is not available for the selected node.'); + return t(messages[capability?.code] || 'doctorUnsupportedInsertion'); } function getNativeWidgetValues(node, widget) { @@ -175,13 +173,13 @@ export function openLoraInsertionPicker(anchorNode, direction) { const insertedNode = typeof LiteGraph !== 'undefined' ? LiteGraph.createNode('LoraLoader') : null; if (!insertedNode) { - alert(window.anomalous_browser_lang === 'zh' ? '无法创建标准 LoRA Loader 节点。' : 'Unable to create a standard LoRA Loader node.'); + alert(t('doctorCreateLoraFailed')); return; } const modelWidget = findModelComboWidget(insertedNode); if (!modelWidget || getNativeWidgetValues(insertedNode, modelWidget).length === 0) { - alert(window.anomalous_browser_lang === 'zh' ? 'LoRA 列表尚未就绪,请刷新 ComfyUI 模型列表后重试。' : 'The LoRA list is not ready. Refresh ComfyUI model lists and try again.'); + alert(t('doctorLoraNotReady')); return; } @@ -204,7 +202,7 @@ if (!this.assistantPanelInitialized) { if (!placeholder || !nodeContent || !app.graph || !app.graph._nodes) return; if (!node) { - placeholder.innerHTML = `
🤖
${window.anomalous_browser_lang === 'zh' ? '请在画布中点击选中任意节点' : 'Please select any node in the canvas'}
`; + placeholder.innerHTML = `
🤖
${t('assistantSelectNode')}
`; placeholder.style.display = 'flex'; nodeContent.style.display = 'none'; nodeContent.innerHTML = ''; @@ -224,7 +222,7 @@ for (const w of node.widgets) { const canInsert = insertionCapabilities.before.supported || insertionCapabilities.after.supported; if (modelWidgets.length === 0 && !canInsert) { - placeholder.innerHTML = `
⚠️
${window.anomalous_browser_lang === 'zh' ? '该节点没有受支持的模型参数' : 'No supported model parameter on this node'}
${escapeHtml(node.type || '')}
`; + placeholder.innerHTML = `
⚠️
${t('assistantNoModelParameter')}
${escapeHtml(node.type || '')}
`; placeholder.style.display = 'flex'; nodeContent.style.display = 'none'; nodeContent.innerHTML = ''; @@ -237,7 +235,7 @@ for (const w of node.widgets) { const titleBar = document.createElement('div'); titleBar.style.cssText = 'margin:14px 16px 0;padding:16px;border:1px solid rgba(138,180,248,0.16);border-radius:14px;background:linear-gradient(135deg,rgba(48,58,86,0.72),rgba(20,22,31,0.92));display:flex;align-items:center;gap:12px;flex-shrink:0;box-shadow:0 12px 30px rgba(0,0,0,0.18);'; - titleBar.innerHTML = `🤖${window.anomalous_browser_lang === 'zh' ? '当前选中节点' : 'Selected node'}`; + titleBar.innerHTML = `🤖${t('assistantSelectedNode')}`; titleBar.querySelector('.ast-title').textContent = node.title || node.type || 'Node'; titleBar.querySelector('.ast-type').textContent = node.type || ''; nodeContent.appendChild(titleBar); @@ -245,7 +243,7 @@ for (const w of node.widgets) { const quickActions = document.createElement('div'); quickActions.style.cssText = 'padding:14px 16px 4px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;flex-shrink:0;'; const actionsLabel = document.createElement('div'); - actionsLabel.textContent = window.anomalous_browser_lang === 'zh' ? '快捷操作' : 'Quick actions'; + actionsLabel.textContent = t('assistantQuickActions'); actionsLabel.style.cssText = 'grid-column:1/-1;color:#8b91a3;font-size:10px;font-weight:750;letter-spacing:0.1em;text-transform:uppercase;padding:0 2px 2px;'; quickActions.appendChild(actionsLabel); @@ -282,13 +280,13 @@ for (const w of node.widgets) { for (const widget of modelWidgets) { const widgetLabel = modelWidgets.length > 1 && widget.name - ? (window.anomalous_browser_lang === 'zh' ? `更换 ${widget.name}` : `Change ${widget.name}`) - : (window.anomalous_browser_lang === 'zh' ? '更换当前模型' : 'Change current model'); + ? t('assistantChangeWidget', { name: widget.name }) + : t('assistantChangeCurrent'); const pickerType = inferPickerModelType(node, widget); makeActionButton({ icon: '⇄', label: widgetLabel, - hint: window.anomalous_browser_lang === 'zh' ? `${pickerType.label} · 可视化选择` : `${pickerType.label} · Visual picker`, + hint: t('assistantVisualPicker', { type: pickerType.label }), accent: 'linear-gradient(135deg,rgba(245,124,0,0.96),rgba(255,82,82,0.82))', onClick: () => this._openGalleryReplacer(node, widget), primary: true, @@ -296,16 +294,16 @@ for (const w of node.widgets) { } makeActionButton({ icon: '←', - label: window.anomalous_browser_lang === 'zh' ? '前方插入 LoRA' : 'Insert LoRA before', - hint: window.anomalous_browser_lang === 'zh' ? '接入节点输入端' : 'Connect to node inputs', + label: t('assistantInsertBefore'), + hint: t('assistantConnectInputs'), accent: 'linear-gradient(135deg,rgba(25,118,210,0.9),rgba(80,110,230,0.82))', onClick: () => this.openLoraInsertionPicker(node, 'before'), capability: insertionCapabilities.before, }); makeActionButton({ icon: '→', - label: window.anomalous_browser_lang === 'zh' ? '后方插入 LoRA' : 'Insert LoRA after', - hint: window.anomalous_browser_lang === 'zh' ? '接入节点输出端' : 'Connect to node outputs', + label: t('assistantInsertAfter'), + hint: t('assistantConnectOutputs'), accent: 'linear-gradient(135deg,rgba(0,137,123,0.92),rgba(67,160,71,0.82))', onClick: () => this.openLoraInsertionPicker(node, 'after'), capability: insertionCapabilities.after, @@ -392,17 +390,16 @@ export function renderGlobalDashboard() { ${count} `; }; - const zh = window.anomalous_browser_lang === 'zh'; statsRow.innerHTML = ` - ${createBadge(zh?'总模型':'Total', total, '#aaa', 'rgba(255,255,255,0.05)')} - ${createBadge(zh?'🟢 健康':'🟢 Healthy', healthy, '#28a745', 'rgba(40, 167, 69, 0.1)')} - ${createBadge(zh?'🔴 缺失':'🔴 Missing', missing, '#ff6b6b', 'rgba(220, 53, 69, 0.1)')} + ${createBadge(t('doctorTotal'), total, '#aaa', 'rgba(255,255,255,0.05)')} + ${createBadge(t('doctorHealthy'), healthy, '#28a745', 'rgba(40, 167, 69, 0.1)')} + ${createBadge(t('doctorMissing'), missing, '#ff6b6b', 'rgba(220, 53, 69, 0.1)')} `; if (total === 0) { content.innerHTML = `
👻
- ${zh ? '当前工作流没有任何模型节点' : 'No models found in this workflow'} + ${t('doctorNoWorkflowModels')}
`; return; } @@ -443,11 +440,11 @@ for (const data of missingNodesData) { right.style.cssText = 'display:flex; align-items:center; gap:12px;'; if (isHealthy && exactMatch && exactMatch !== val) { - right.innerHTML = `
🟡 ${zh?'自动重定向':'Auto-Redirected'}
${escapeHtml(exactMatch.split(/[\/]/).pop())}
`; + right.innerHTML = `
🟡 ${t('doctorAutoRedirected')}
${escapeHtml(exactMatch.split(/[\/]/).pop())}
`; } else if (isHealthy) { - right.innerHTML = `
🟢 ${zh?'正常':'Ready'}
`; + right.innerHTML = `
🟢 ${t('doctorReady')}
`; } else { - right.innerHTML = `
🔴 ${zh?'丢失':'Missing'}
`; + right.innerHTML = `
🔴 ${t('doctorMissingStatus')}
`; } top.appendChild(left); @@ -458,7 +455,7 @@ for (const data of missingNodesData) { actionRow.style.cssText = 'display:flex; gap:10px; margin-top:16px; margin-left:8px;'; const civitaiBtn = document.createElement('button'); - civitaiBtn.innerHTML = zh ? '🌐 C站' : '🌐 Civitai'; + civitaiBtn.textContent = t('doctorCivitai'); civitaiBtn.style.cssText = 'padding:8px 16px; background:rgba(255,255,255,0.1); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;'; civitaiBtn.onmouseover = () => civitaiBtn.style.background = 'rgba(255,255,255,0.2)'; civitaiBtn.onmouseout = () => civitaiBtn.style.background = 'rgba(255,255,255,0.1)'; @@ -480,12 +477,12 @@ for (const data of missingNodesData) { if (!isHealthy) { const deepScanBtn = document.createElement('button'); - deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan'; + deepScanBtn.textContent = t('doctorDeepHashScan'); deepScanBtn.style.cssText = 'padding:8px 16px; background:#1a73e8; color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;'; deepScanBtn.onmouseover = () => deepScanBtn.style.background = '#1557b0'; deepScanBtn.onmouseout = () => deepScanBtn.style.background = '#1a73e8'; deepScanBtn.onclick = async () => { - deepScanBtn.innerText = zh ? '⏳ 正在启动扫描引擎...' : '⏳ Starting scan engine...'; + deepScanBtn.textContent = t('doctorScanStarting'); deepScanBtn.disabled = true; deepScanBtn.style.opacity = '0.7'; @@ -509,20 +506,18 @@ if (!isHealthy) { if (statusData.scanning) { let filename = statusData.filename || ''; if (filename.length > 20) filename = filename.substring(0, 10) + '...' + filename.substring(filename.length - 7); - deepScanBtn.innerText = zh - ? `⏳ 扫描中 (${statusData.current}/${statusData.total}) ${filename}` - : `⏳ Scanning (${statusData.current}/${statusData.total}) ${filename}`; + deepScanBtn.textContent = t('doctorScanning', { current: statusData.current, total: statusData.total, filename }); setTimeout(pollStatus, 500); } else { if (statusData.error) { - alert(zh ? '❌ 扫描过程中发生错误: ' + statusData.error : '❌ Scan error: ' + statusData.error); - deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan'; + alert(t('doctorScanError') + statusData.error); + deepScanBtn.textContent = t('doctorDeepHashScan'); deepScanBtn.disabled = false; deepScanBtn.style.opacity = '1'; return; } - deepScanBtn.innerText = zh ? '⏳ 正在匹配并替换飘红节点...' : '⏳ Matching and resolving red nodes...'; + deepScanBtn.textContent = t('doctorMatching'); if (window.anomalous_reload_hashes) { await window.anomalous_reload_hashes(); @@ -542,24 +537,22 @@ if (window.anomalous_resolve_all_missing_nodes) { } } - const scanInfo = zh - ? `共深度扫描了 ${statusData.total} 个缺失信息的模型。` - : `Deep scanned ${statusData.total} models with missing info.`; + const scanInfo = t('doctorDeepScanSummary', { total: statusData.total }); if (stillMissing) { - alert(zh ? `❌ 扫描结束,本地未匹配到模型。\n\n${scanInfo}\n\n这说明模型可能真的不在您的硬盘里,或者您删除了原本记录着哈希的源文件。\n请点击卡片上的【🌐 C站】去云端下载。` : `❌ Scan finished, but no local match found.\n\n${scanInfo}\n\nThis means the model is truly missing from your disk, or the original source file with hash was deleted.\nPlease click [🌐 Civitai] to download it from the cloud.`); + alert(t('doctorNoLocalMatch', { summary: scanInfo })); } else { - alert(zh ? `✅ 深度扫描成功!已自动修复该节点!\n\n${scanInfo}` : `✅ Deep Scan successful! Node auto-healed!\n\n${scanInfo}`); + alert(t('doctorScanSuccess', { summary: scanInfo })); } this.renderGlobalDashboard(); - deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan'; + deepScanBtn.textContent = t('doctorDeepHashScan'); deepScanBtn.disabled = false; deepScanBtn.style.opacity = '1'; } } catch (err) { - alert(zh ? '❌ 状态轮询出错: ' + err.message : '❌ Poll Error: ' + err.message); - deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan'; + alert(t('doctorPollError') + err.message); + deepScanBtn.textContent = t('doctorDeepHashScan'); deepScanBtn.disabled = false; deepScanBtn.style.opacity = '1'; } @@ -567,15 +560,15 @@ if (stillMissing) { setTimeout(pollStatus, 500); } catch(e) { - alert(zh ? '❌ 扫描出错: ' + e.message : '❌ Scan Error: ' + e.message); - deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan'; + alert(t('doctorScanFailed') + e.message); + deepScanBtn.textContent = t('doctorDeepHashScan'); deepScanBtn.disabled = false; deepScanBtn.style.opacity = '1'; } }; const manualBtn = document.createElement('button'); - manualBtn.innerHTML = zh ? '🔀 手动替换' : '🔀 Manual Replace'; + manualBtn.textContent = t('doctorManualReplace'); manualBtn.style.cssText = 'padding:8px 16px; background:rgba(255,255,255,0.1); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;'; manualBtn.onmouseover = () => manualBtn.style.background = 'rgba(255,255,255,0.2)'; manualBtn.onmouseout = () => manualBtn.style.background = 'rgba(255,255,255,0.1)'; @@ -587,7 +580,7 @@ if (stillMissing) { actionRow.appendChild(manualBtn); const pathText = document.createElement('div'); - pathText.innerText = `${zh?'原路径':'Original'}: ${val}`; + pathText.textContent = t('doctorOriginalPath', { path: val }); pathText.style.cssText = 'margin-top:12px; margin-left:8px; color:rgba(255,255,255,0.3); font-size:11px; font-family:monospace; word-break:break-all;'; item.appendChild(pathText); } @@ -637,7 +630,7 @@ if (!this._assistantPanelHooked) { const placeholder = document.createElement('div'); placeholder.id = 'anomalous-assistant-placeholder'; placeholder.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;color:#666;font-size:15px;gap:12px;padding:40px;text-align:center;'; - placeholder.innerHTML = `
🤖
${window.anomalous_browser_lang === 'zh' ? '请在画布中点击选中任意节点' : 'Please select any node in the canvas'}
`; + placeholder.innerHTML = `
🤖
${t('assistantSelectNode')}
`; this.assistantPanel.appendChild(placeholder); const nodeContent = document.createElement('div'); @@ -657,7 +650,7 @@ export function renderAssistantModelCard(node, w, container) { // Preview image const previewBox = document.createElement('div'); previewBox.style.cssText = 'width:100%;aspect-ratio:1.65;max-height:260px;background:radial-gradient(circle at 50% 20%,#252a3b,#0b0c10 70%);border-radius:10px;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,0.05);'; - previewBox.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '加载预览...' : 'Loading preview...'}`; + previewBox.innerHTML = `${t('doctorLoadingPreview')}`; wrapper.appendChild(previewBox); // Name and path @@ -689,7 +682,7 @@ export function renderAssistantModelCard(node, w, container) { const actionRow = document.createElement('div'); actionRow.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;'; const profileBtn = document.createElement('button'); - profileBtn.innerText = window.anomalous_browser_lang === 'zh' ? '📖 查看档案' : '📖 View Profile'; + profileBtn.textContent = t('doctorProfile'); profileBtn.style.cssText = 'flex:1;padding:10px 12px;background:rgba(138,180,248,0.09);color:#a9c7ff;border:1px solid rgba(138,180,248,0.22);border-radius:9px;cursor:pointer;font-size:12px;font-weight:700;transition:filter 0.2s;'; profileBtn.onmouseover = () => profileBtn.style.filter = 'brightness(1.2)'; profileBtn.onmouseout = () => profileBtn.style.filter = 'brightness(1)'; @@ -719,7 +712,7 @@ if (isVid) { previewBox.appendChild(img); } } else { - previewBox.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '暂无预览图' : 'No preview'}`; + previewBox.innerHTML = `${t('doctorNoPreview')}`; } // Profile button links to detail @@ -747,7 +740,7 @@ if (isVid) { badgeRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;'; const b = document.createElement('span'); b.style.cssText = 'background:rgba(0,255,204,0.08);border:1px solid rgba(0,255,204,0.15);color:#77e6cf;padding:4px 8px;border-radius:999px;font-size:10px;'; - b.innerText = `${window.anomalous_browser_lang === 'zh' ? '基础模型' : 'Base'} · ${meta.baseModel}`; + b.textContent = `${t('doctorBase')} · ${meta.baseModel}`; badgeRow.appendChild(b); metaZone.appendChild(badgeRow); } @@ -759,7 +752,7 @@ if (triggers && triggers.length > 0) { trigSection.style.cssText = 'background:rgba(255,255,255,0.04);border-radius:6px;padding:10px 12px;'; const trigTitle = document.createElement('div'); trigTitle.style.cssText = 'color:#aaa;font-size:11px;margin-bottom:8px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;'; - trigTitle.innerText = window.anomalous_browser_lang === 'zh' ? '触发词' : 'Trigger Words'; + trigTitle.textContent = t('doctorTriggerWords'); const tagList = document.createElement('div'); tagList.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;'; const words = Array.isArray(triggers) ? triggers : [triggers]; @@ -767,7 +760,7 @@ if (triggers && triggers.length > 0) { const tag = document.createElement('span'); tag.style.cssText = 'background:rgba(255,193,7,0.12);color:#ffc107;padding:3px 8px;border-radius:4px;font-size:12px;cursor:pointer;'; tag.innerText = word; - tag.title = window.anomalous_browser_lang === 'zh' ? '点击复制' : 'Click to copy'; + tag.title = t('doctorClickCopy'); tag.onclick = () => { navigator.clipboard.writeText(word).then(() => { const o = tag.innerText; tag.innerText = '✅'; setTimeout(() => tag.innerText = o, 1000); }); }; @@ -775,9 +768,9 @@ if (triggers && triggers.length > 0) { }); const copyAll = document.createElement('button'); copyAll.style.cssText = 'background:transparent;border:1px solid #444;color:#888;border-radius:4px;padding:3px 8px;font-size:11px;cursor:pointer;margin-top:4px;'; - copyAll.innerText = window.anomalous_browser_lang === 'zh' ? '📋 全部复制' : '📋 Copy All'; + copyAll.textContent = t('doctorCopyAll'); copyAll.onclick = () => { - navigator.clipboard.writeText(words.join(', ')).then(() => { copyAll.innerText = '✅'; setTimeout(() => copyAll.innerText = window.anomalous_browser_lang === 'zh' ? '📋 全部复制' : '📋 Copy All', 1500); }); + navigator.clipboard.writeText(words.join(', ')).then(() => { copyAll.textContent = '✅'; setTimeout(() => copyAll.textContent = t('doctorCopyAll'), 1500); }); }; trigSection.appendChild(trigTitle); trigSection.appendChild(tagList); @@ -792,7 +785,7 @@ if (textNotes) { notesCard.style.cssText = 'background:linear-gradient(135deg,#262522 0%,#202124 100%);border:1px solid #3c4043;border-left:4px solid #a38d53;border-radius:4px 8px 8px 4px;padding:12px 14px;'; const notesTitle = document.createElement('div'); notesTitle.style.cssText = 'color:#a38d53;font-size:11px;font-weight:bold;margin-bottom:6px;'; - notesTitle.innerText = window.anomalous_browser_lang === 'zh' ? '📝 我的备注' : '📝 My Notes'; + notesTitle.textContent = t('doctorNotes'); const notesText = document.createElement('div'); notesText.style.cssText = 'color:#d4c4a0;font-size:13px;line-height:1.6;white-space:pre-wrap;'; notesText.innerText = textNotes; @@ -806,7 +799,7 @@ if (textNotes) { const resolvedFilename = (d.status === 'success' && d.model) ? (d.model.filename || filename) : filename; this._loadAssistantHistory(resolvedFilename, metaZone, d.status === 'success' ? d.model : null); }).catch(() => { - previewBox.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '无法加载预览' : 'Failed to load preview'}`; + previewBox.innerHTML = `${t('assistantPreviewFailed')}`; // Still try to load history gallery by filename this._loadAssistantHistory(filename, metaZone, null); @@ -828,13 +821,13 @@ export function _loadAssistantHistory(filename, container, model) { sectionHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;'; const sectionTitle = document.createElement('div'); sectionTitle.style.cssText = 'color:#aaa;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;'; - sectionTitle.innerText = window.anomalous_browser_lang === 'zh' ? `🖼️ 历史生成图 (${images.length})` : `🖼️ History (${images.length})`; + sectionTitle.textContent = t('assistantHistoryTitle', { count: images.length }); sectionHeader.appendChild(sectionTitle); // "View all" button if model is available if (model) { const viewAllBtn = document.createElement('button'); - viewAllBtn.innerText = window.anomalous_browser_lang === 'zh' ? '查看全部 →' : 'View All →'; + viewAllBtn.textContent = t('assistantViewAll'); viewAllBtn.style.cssText = 'background:transparent;border:1px solid rgba(138,180,248,0.3);color:#8AB4F8;font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.2s;'; viewAllBtn.onmouseover = () => { viewAllBtn.style.background = 'rgba(138,180,248,0.1)'; }; viewAllBtn.onmouseout = () => { viewAllBtn.style.background = 'transparent'; }; @@ -871,7 +864,7 @@ if (model) { card.appendChild(imgEl); } if (img.workflow) { - card.title = window.anomalous_browser_lang === 'zh' ? '点击恢复此工作流' : 'Click to restore workflow'; + card.title = t('assistantRestoreWorkflow'); card.onclick = () => { try { const wf = typeof img.workflow === 'string' ? JSON.parse(img.workflow) : img.workflow; @@ -879,7 +872,7 @@ if (img.workflow) { } catch (e) { } }; } else if (model) { - card.title = window.anomalous_browser_lang === 'zh' ? '点击查看完整图库' : 'Click to view full gallery'; + card.title = t('assistantViewGallery'); card.onclick = () => this.showGeneratedGallery(model); } grid.appendChild(card); @@ -890,12 +883,11 @@ if (img.workflow) { } export function _openGalleryReplacer(node, w, options = {}) { - const zh = window.anomalous_browser_lang === 'zh'; const mode = options.mode === 'insert' ? 'insert' : 'replace'; const pickerType = inferPickerModelType(node, w, options); const validPaths = getNativeWidgetValues(node, w); if (!validPaths.length) { - alert(zh ? '没有可供选择的兼容模型。' : 'No compatible models are available.'); + alert(t('pickerNoCompatible')); return; } @@ -942,15 +934,13 @@ export function _openGalleryReplacer(node, w, options = {}) { const headerCopy = document.createElement('div'); headerCopy.style.cssText = 'display:flex;flex-direction:column;gap:3px;'; const eyebrow = document.createElement('div'); - eyebrow.textContent = zh ? '节点助手 · 模型选择器' : 'Node Assistant · Model Picker'; + eyebrow.textContent = t('pickerEyebrow'); eyebrow.style.cssText = 'font-size:10px;color:#8fa8da;letter-spacing:0.12em;text-transform:uppercase;font-weight:750;'; const title = document.createElement('h2'); title.style.cssText = 'margin:0;font-size:19px;line-height:1.15;'; title.textContent = mode === 'insert' - ? (options.direction === 'before' - ? (zh ? '⬅ 在节点前方插入 LoRA' : '⬅ Insert LoRA Before Node') - : (zh ? '在节点后方插入 LoRA ➡' : 'Insert LoRA After Node ➡')) - : (zh ? '🔀 更换当前模型' : '🔀 Change Current Model'); + ? (options.direction === 'before' ? t('pickerInsertBeforeTitle') : t('pickerInsertAfterTitle')) + : t('pickerChangeTitle'); const typeBadge = document.createElement('span'); typeBadge.textContent = pickerType.label; typeBadge.style.cssText = 'padding:5px 10px;border-radius:20px;background:rgba(138,180,248,0.1);border:1px solid rgba(138,180,248,0.25);color:#a9c7ff;font-size:10px;font-weight:750;'; @@ -971,7 +961,7 @@ export function _openGalleryReplacer(node, w, options = {}) { const folderPanel = document.createElement('aside'); folderPanel.style.cssText = 'background:rgba(20,22,29,0.92);border:1px solid rgba(255,255,255,0.075);border-radius:13px;overflow:auto;padding:10px;box-shadow:0 14px 35px rgba(0,0,0,0.18);'; const folderTitle = document.createElement('div'); - folderTitle.textContent = zh ? '📁 文件夹' : '📁 Folders'; + folderTitle.textContent = t('pickerFolders'); folderTitle.style.cssText = 'font-size:13px;font-weight:700;color:#ddd;padding:8px 10px 10px;'; const folderList = document.createElement('div'); folderList.style.cssText = 'display:flex;flex-direction:column;gap:3px;'; @@ -983,20 +973,20 @@ export function _openGalleryReplacer(node, w, options = {}) { toolbar.style.cssText = 'display:flex;gap:10px;padding:12px;border-bottom:1px solid #333;flex-wrap:wrap;align-items:center;'; const searchInput = document.createElement('input'); searchInput.type = 'search'; - searchInput.placeholder = zh ? '🔍 搜索名称或完整路径…' : '🔍 Search name or full path…'; + searchInput.placeholder = t('pickerSearch'); searchInput.style.cssText = 'flex:1;min-width:220px;padding:10px 12px;border-radius:9px;border:1px solid rgba(255,255,255,0.1);background:#222631;color:#fff;font-size:13px;outline:none;'; const baseFilterSelect = document.createElement('select'); baseFilterSelect.style.cssText = `display:${pickerType.isLora ? 'block' : 'none'};padding:10px 12px;border-radius:9px;border:1px solid rgba(0,220,180,0.2);background:#1d292b;color:#9be8d9;font-size:12px;max-width:230px;`; const allBaseOption = document.createElement('option'); allBaseOption.value = ''; - allBaseOption.textContent = zh ? '主模型:全部类型' : 'Main model: All types'; + allBaseOption.textContent = t('pickerMainAll'); baseFilterSelect.appendChild(allBaseOption); const sortSelect = document.createElement('select'); sortSelect.style.cssText = 'padding:10px 12px;border-radius:9px;border:1px solid rgba(255,255,255,0.1);background:#222631;color:#fff;font-size:12px;'; [ - ['name-asc', zh ? '名称 A–Z' : 'Name A–Z'], - ['name-desc', zh ? '名称 Z–A' : 'Name Z–A'], - ['folder-asc', zh ? '按文件夹' : 'By Folder'], + ['name-asc', t('pickerNameAsc')], + ['name-desc', t('pickerNameDesc')], + ['folder-asc', t('pickerByFolder')], ].forEach(([value, label]) => { const option = document.createElement('option'); option.value = value; @@ -1009,7 +999,7 @@ export function _openGalleryReplacer(node, w, options = {}) { content.appendChild(toolbar); const loadingText = document.createElement('div'); - loadingText.textContent = zh ? '⏳ 正在加载模型封面…' : '⏳ Loading model covers…'; + loadingText.textContent = t('pickerLoadingCovers'); loadingText.style.cssText = 'color:#888;font-size:13px;padding:10px 14px 0;'; const gridScroll = document.createElement('div'); gridScroll.style.cssText = 'overflow:auto;min-height:0;flex:1;padding:14px;'; @@ -1025,13 +1015,13 @@ export function _openGalleryReplacer(node, w, options = {}) { const selectionText = document.createElement('div'); selectionText.style.cssText = 'min-width:0;flex:1;color:#aaa;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; const cancelBtn = document.createElement('button'); - cancelBtn.textContent = zh ? '取消' : 'Cancel'; + cancelBtn.textContent = t('pickerCancel'); cancelBtn.style.cssText = 'padding:10px 18px;background:#333;color:#ddd;border:1px solid #555;border-radius:7px;cursor:pointer;'; cancelBtn.onclick = closeModal; const confirmBtn = document.createElement('button'); confirmBtn.textContent = mode === 'insert' - ? (zh ? '插入并自动连线' : 'Insert & Auto-Connect') - : (zh ? '确认替换' : 'Confirm Replacement'); + ? t('pickerInsertConfirm') + : t('pickerReplaceConfirm'); confirmBtn.style.cssText = 'padding:10px 20px;background:#1976d2;color:#fff;border:none;border-radius:7px;cursor:pointer;font-weight:700;'; footer.append(selectionText, cancelBtn, confirmBtn); modal.appendChild(footer); @@ -1059,8 +1049,8 @@ export function _openGalleryReplacer(node, w, options = {}) { const updateSelection = () => { selectionText.textContent = selectedPath - ? `${zh ? '已选择' : 'Selected'}: ${normalizePath(selectedPath)}` - : (zh ? '请选择一个模型。节点连接和强度不会被改动。' : 'Choose a model. Existing connections and strengths will be preserved.'); + ? t('pickerSelected', { path: normalizePath(selectedPath) }) + : t('pickerChooseModel'); confirmBtn.disabled = !selectedPath || applying; confirmBtn.style.opacity = confirmBtn.disabled ? '0.45' : '1'; confirmBtn.style.cursor = confirmBtn.disabled ? 'not-allowed' : 'pointer'; @@ -1103,21 +1093,13 @@ export function _openGalleryReplacer(node, w, options = {}) { compatibilityHint.style.display = 'flex'; if (mainBaseModel && selectedBaseFamily) { - compatibilityHint.textContent = zh - ? `✨ 已根据主模型 ${mainBaseModel} 自动筛选兼容 LoRA;可在上方切换为全部类型。` - : `✨ Compatible LoRAs are filtered for the main model ${mainBaseModel}. Use the filter above to show all types.`; + compatibilityHint.textContent = t('pickerCompatibleMain', { name: mainBaseModel }); } else if (preferredBaseModel && selectedBaseFamily) { - compatibilityHint.textContent = zh - ? `✨ 未找到已连接主模型,已根据当前 LoRA 的 ${preferredBaseModel} 标注筛选。` - : `✨ No connected main model was identified, so the current LoRA tag ${preferredBaseModel} is used.`; + compatibilityHint.textContent = t('pickerCompatibleTag', { name: preferredBaseModel }); } else if (mainBaseModel) { - compatibilityHint.textContent = zh - ? `ℹ 已识别主模型 ${mainBaseModel},但没有同类 LoRA 标注,因此暂时显示全部。` - : `ℹ Main model ${mainBaseModel} was identified, but no matching LoRA tags were found, so all models remain visible.`; + compatibilityHint.textContent = t('pickerCompatibleNoTags', { name: mainBaseModel }); } else { - compatibilityHint.textContent = zh - ? 'ℹ 未识别到主模型类型,暂时显示全部 LoRA;你仍可手动按基础模型筛选。' - : 'ℹ The main model type could not be identified. All LoRAs remain visible and can be filtered manually.'; + compatibilityHint.textContent = t('pickerCompatibleUnknown'); } }; @@ -1127,7 +1109,7 @@ export function _openGalleryReplacer(node, w, options = {}) { folders.forEach(folderPath => { const button = document.createElement('button'); const depth = folderPath ? folderPath.split('/').length - 1 : 0; - const label = folderPath ? folderPath.split('/').pop() : (zh ? '全部模型' : 'All Models'); + const label = folderPath ? folderPath.split('/').pop() : t('pickerAllModels'); button.textContent = `${folderPath ? '📂' : '📦'} ${label} (${folderCounts.get(folderPath) || 0})`; button.title = folderPath || label; button.style.cssText = `text-align:left;padding:7px 8px 7px ${8 + depth * 14}px;border-radius:6px;border:none;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:${selectedFolder === folderPath ? '#fff' : '#aaa'};background:${selectedFolder === folderPath ? 'rgba(25,118,210,0.45)' : 'transparent'};`; @@ -1153,7 +1135,7 @@ export function _openGalleryReplacer(node, w, options = {}) { else if (sortSelect.value === 'folder-asc') paths.sort((a, b) => collator.compare(normalizePath(a), normalizePath(b))); else paths.sort((a, b) => collator.compare(getName(a), getName(b))); - resultCount.textContent = zh ? `${paths.length} 个结果` : `${paths.length} results`; + resultCount.textContent = t('pickerResults', { count: paths.length }); stopMedia(grid); grid.replaceChildren(); const generation = ++renderGeneration; @@ -1197,7 +1179,7 @@ export function _openGalleryReplacer(node, w, options = {}) { } if (isCurrent) { const badge = document.createElement('span'); - badge.textContent = zh ? '当前' : 'Current'; + badge.textContent = t('pickerCurrent'); badge.style.cssText = 'position:absolute;top:6px;left:6px;background:rgba(255,193,7,0.92);color:#111;padding:3px 6px;border-radius:4px;font-size:10px;font-weight:800;'; previewBox.appendChild(badge); } @@ -1210,7 +1192,7 @@ export function _openGalleryReplacer(node, w, options = {}) { if (baseModel) { const baseBadge = document.createElement('span'); baseBadge.textContent = baseModel; - baseBadge.title = `${zh ? '基础模型' : 'Base model'}: ${baseModel}`; + baseBadge.title = `${t('pickerBaseModel')}: ${baseModel}`; baseBadge.style.cssText = 'max-width:100%;padding:3px 6px;border-radius:5px;background:rgba(7,50,43,0.9);border:1px solid rgba(70,220,185,0.25);color:#8ce1cf;font-size:9px;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 3px 8px rgba(0,0,0,0.22);'; badgeStack.appendChild(baseBadge); } @@ -1220,7 +1202,7 @@ export function _openGalleryReplacer(node, w, options = {}) { name.title = normalizePath(path); name.style.cssText = 'padding:9px 9px 3px;font-size:12px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:700;'; const folder = document.createElement('div'); - folder.textContent = getFolder(path) || (zh ? '根目录' : 'Root'); + folder.textContent = getFolder(path) || t('pickerRoot'); folder.title = getFolder(path); folder.style.cssText = 'padding:0 9px 9px;font-size:9px;color:#747b8b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; card.append(previewBox, name, folder); @@ -1252,7 +1234,7 @@ export function _openGalleryReplacer(node, w, options = {}) { if (paths.length) requestAnimationFrame(renderChunk); else { const empty = document.createElement('div'); - empty.textContent = zh ? '没有符合条件的模型。' : 'No models match the current filters.'; + empty.textContent = t('pickerNoMatches'); empty.style.cssText = 'color:#777;padding:30px;text-align:center;grid-column:1/-1;'; grid.appendChild(empty); } @@ -1300,7 +1282,7 @@ export function _openGalleryReplacer(node, w, options = {}) { applying = false; updateSelection(); console.error('[Anomalous] Failed to apply model choice:', error); - alert(zh ? `操作失败:${error.message}` : `Operation failed: ${error.message}`); + alert(t('pickerOperationFailed') + error.message); } }; @@ -1346,7 +1328,7 @@ export function _openGalleryReplacer(node, w, options = {}) { }).catch(error => { if (!modal.isConnected) return; console.error('[Anomalous] Failed to load model previews:', error); - loadingText.textContent = zh ? '⚠️ 封面加载失败,仍可按名称选择模型。' : '⚠️ Covers failed to load; models remain selectable by name.'; + loadingText.textContent = t('pickerCoversFailed'); configureBaseFilter(); renderFolders(); }); @@ -1373,7 +1355,7 @@ for (let w of node.widgets) { if (!isHealthy) { missingNodes++; const nodeTitle = document.createElement('div'); - nodeTitle.textContent = `${window.anomalous_browser_lang === 'zh' ? '节点' : 'Node'}: ${node.title || node.type}`; + nodeTitle.textContent = `${t('doctorNodeLabel')}: ${node.title || node.type}`; nodeTitle.style.color = '#8AB4F8'; nodeTitle.style.fontWeight = 'bold'; nodeTitle.style.marginTop = '10px'; @@ -1386,6 +1368,12 @@ if (!isHealthy) { } if (missingNodes === 0) { - content.innerHTML = `
${window.anomalous_browser_lang === 'zh' ? '🎉 太棒了!当前工作流中所有模型均在本地就绪。' : '🎉 Awesome! All models in this workflow are healthy.'}
`; + const healthyMessage = document.createElement('div'); + healthyMessage.style.color = '#28a745'; + healthyMessage.style.textAlign = 'center'; + healthyMessage.style.padding = '20px'; + healthyMessage.style.fontSize = '16px'; + healthyMessage.textContent = t('doctorAllHealthy'); + content.appendChild(healthyMessage); } } diff --git a/web/modules/ui_notebooks.js b/web/modules/ui_notebooks.js index 0e0141c..a607e3b 100644 --- a/web/modules/ui_notebooks.js +++ b/web/modules/ui_notebooks.js @@ -4,14 +4,10 @@ */ import { app } from "../../../scripts/app.js"; -import { i18n } from './locales.js'; +import { translate } from './locales.js'; import { escapeHtml } from './safe_dom.js'; -const t = (key) => { - let lang = window.anomalous_browser_lang || 'zh'; - if (lang.startsWith('en')) lang = 'en'; - return (i18n[lang] && i18n[lang][key]) ? i18n[lang][key] : (i18n['zh'][key] || key); -}; +const t = (key, params) => translate(key, params); function restoreWorkspaceReturnPanel(owner) { const state = owner.workspaceReturnState; @@ -704,7 +700,7 @@ export function sendNotebookToCanvas() { if (!this.currentNotebook) return; const data = this.currentNotebook.data || {}; if (!data.mainModel) { - alert(window.anomalous_browser_lang === 'zh' ? "请先选择一个主模型。" : "Please select a Main Model first."); + alert(t('notebookSelectMain')); return; }