From 09d3eccf403ba23de169c13e3fe47607cdf764f2 Mon Sep 17 00:00:00 2001
From: DemonGatanjieu <1027066412@qq.com>
Date: Thu, 6 Aug 2026 17:11:17 +0800
Subject: [PATCH] Migrate detail doctor and notebook i18n
---
.agents/logs/ai_changelog.md | 23 +++
ARCHITECTURE.md | 2 +
web/modules/locales.js | 282 +++++++++++++++++++++++++++++++++++
web/modules/ui_detail.js | 164 +++++++++++---------
web/modules/ui_doctor.js | 220 +++++++++++++--------------
web/modules/ui_notebooks.js | 10 +-
6 files changed, 509 insertions(+), 192 deletions(-)
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;
}