Spaces:
Configuration error
Configuration error
| import { app } from "../../scripts/app.js"; | |
| import { $el } from "../../scripts/ui.js"; | |
| import { LOCALES } from "./LocaleMap.js"; | |
| import { applyMenuTranslation, observeFactory } from "./MenuTranslate.js"; | |
| // Translation Utils | |
| export class TUtils { | |
| static LOCALE_ID = "AGL.Locale"; | |
| static LOCALE_ID_LAST = "AGL.LocaleLast"; | |
| static T = { | |
| Menu: {}, | |
| Nodes: {}, | |
| NodeCategory: {}, | |
| Locales: LOCALES, | |
| }; | |
| static ELS = {}; | |
| static setLocale(locale) { | |
| localStorage[TUtils.LOCALE_ID_LAST] = localStorage.getItem(TUtils.LOCALE_ID) || "en-US"; | |
| localStorage[TUtils.LOCALE_ID] = locale; | |
| setTimeout(() => { | |
| location.reload(); | |
| }, 500); | |
| } | |
| static syncTranslation(OnFinished = () => {}) { | |
| var locale = localStorage.getItem(TUtils.LOCALE_ID) || "en-US"; | |
| if (localStorage.getItem(TUtils.LOCALE_ID) === null) { | |
| // 有可能菜单设置了zh-CN但 loacalStorage为空, 这时不会刷新 | |
| let slocal = localStorage.getItem(`Comfy.Settings.${TUtils.LOCALE_ID}`); | |
| if (slocal) { | |
| locale = slocal.replace(/^"(.*)"$/, "$1"); | |
| } | |
| } | |
| var url = "./agl/get_translation"; | |
| var request = new XMLHttpRequest(); | |
| request.open("post", url, false); | |
| request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); | |
| request.onload = function () { | |
| /* XHR对象获取到返回信息后执行 */ | |
| if (request.status != 200) return; | |
| var resp = JSON.parse(request.responseText); | |
| for (var key in TUtils.T) { | |
| if (key in resp) TUtils.T[key] = resp[key]; | |
| else TUtils.T[key] = {}; | |
| } | |
| TUtils.T.Locales = LOCALES; | |
| // 合并NodeCategory 到 Menu | |
| TUtils.Menu = Object.assign(TUtils.T.Menu, TUtils.T.NodeCategory); | |
| // 提取 Node 中 key 到 Menu | |
| for (let key in TUtils.T.Nodes) { | |
| let node = TUtils.T.Nodes[key]; | |
| TUtils.Menu[key] = node["title"] || key; | |
| } | |
| OnFinished(); | |
| }; | |
| request.send(`locale=${locale}`); | |
| } | |
| static enhandeDrawNodeWidgets() { | |
| let theme = localStorage.getItem("Comfy.Settings.Comfy.ColorPalette") || ""; | |
| theme = theme.replace(/^"(.*)"$/, "$1"); | |
| if (!["dark", "light", "solarized", "arc", "nord", "github", ""].includes(theme)) return; | |
| let drawNodeWidgets = LGraphCanvas.prototype.drawNodeWidgets; | |
| LGraphCanvas.prototype.drawNodeWidgets = function (node, posY, ctx, active_widget) { | |
| if (!node.widgets || !node.widgets.length) { | |
| return 0; | |
| } | |
| const widgets = node.widgets.filter((w) => w.type === "slider"); | |
| widgets.forEach((widget) => { | |
| widget._ori_label = widget.label; | |
| const fixed = widget.options.precision != null ? widget.options.precision : 3; | |
| widget.label = (widget.label || widget.name) + ": " + Number(widget.value).toFixed(fixed).toString(); | |
| }); | |
| let result; | |
| try { | |
| result = drawNodeWidgets.call(this, node, posY, ctx, active_widget); | |
| } finally { | |
| widgets.forEach((widget) => { | |
| widget.label = widget._ori_label; | |
| delete widget._ori_label; | |
| }); | |
| } | |
| return result; | |
| }; | |
| } | |
| static applyNodeTypeTranslationEx(nodeName) { | |
| let nodesT = this.T.Nodes; | |
| var nodeType = LiteGraph.registered_node_types[nodeName]; | |
| let class_type = nodeType.comfyClass ? nodeType.comfyClass : nodeType.type; | |
| if (nodesT.hasOwnProperty(class_type)) { | |
| nodeType.title = nodesT[class_type]["title"] || nodeType.title; | |
| } | |
| } | |
| static applyNodeTypeTranslation(app) { | |
| for (let nodeName in LiteGraph.registered_node_types) { | |
| this.applyNodeTypeTranslationEx(nodeName); | |
| } | |
| } | |
| static applyNodeTranslation(node) { | |
| let keys = ["inputs", "outputs", "widgets"]; | |
| let nodesT = this.T.Nodes; | |
| let class_type = node.constructor.comfyClass ? node.constructor.comfyClass : node.constructor.type; | |
| if (!nodesT.hasOwnProperty(class_type)) { | |
| for (let key of keys) { | |
| if (!node.hasOwnProperty(key)) continue; | |
| node[key].forEach((item) => { | |
| if (item?.hasOwnProperty("name")) item.label = item.name; | |
| }); | |
| } | |
| return; | |
| } | |
| var t = nodesT[class_type]; | |
| for (let key of keys) { | |
| if (!t.hasOwnProperty(key)) continue; | |
| if (!node.hasOwnProperty(key)) continue; | |
| node[key].forEach((item) => { | |
| if (item?.name in t[key]) { | |
| item.label = t[key][item.name]; | |
| } | |
| }); | |
| } | |
| if (t.hasOwnProperty("title")) { | |
| node.title = t["title"]; | |
| node.constructor.title = t["title"]; | |
| } | |
| // 转换 widget 到 input 时需要刷新socket信息 | |
| let addInput = node.addInput; | |
| node.addInput = function (name, type, extra_info) { | |
| var oldInputs = []; | |
| this.inputs?.forEach((i) => oldInputs.push(i.name)); | |
| var res = addInput.apply(this, arguments); | |
| this.inputs?.forEach((i) => { | |
| if (oldInputs.includes(i.name)) return; | |
| if (t["widgets"] && i.widget?.name in t["widgets"]) { | |
| i.label = t["widgets"][i.widget?.name]; | |
| } | |
| }); | |
| return res; | |
| }; | |
| let onInputAdded = node.onInputAdded; | |
| node.onInputAdded = function (slot) { | |
| if (onInputAdded) var res = onInputAdded.apply(this, arguments); | |
| // console.log(slot); | |
| let t = TUtils.T.Nodes[this.comfyClass]; | |
| if (t["widgets"] && slot.name in t["widgets"]) { | |
| slot.label = t["widgets"][slot.name]; | |
| } | |
| if (onInputAdded) return res; | |
| }; | |
| } | |
| static applyNodeDescTranslation(nodeType, nodeData, app) { | |
| let nodesT = this.T.Nodes; | |
| var t = nodesT[nodeType.comfyClass]; | |
| nodeData.description = t?.["description"] || nodeData.description; | |
| } | |
| static applyMenuTranslation(app) { | |
| // 搜索菜单 常驻菜单 | |
| applyMenuTranslation(TUtils.T); | |
| // Queue size 单独处理 | |
| observeFactory(app.ui.menuContainer.querySelector(".drag-handle").childNodes[1], (mutationsList, observer) => { | |
| for (let mutation of mutationsList) { | |
| for (let node of mutation.addedNodes) { | |
| var match = node.data.match(/(Queue size:) (\w+)/); | |
| if (match?.length == 3) { | |
| const t = TUtils.T.Menu[match[1]] ? TUtils.T.Menu[match[1]] : match[1]; | |
| node.data = t + " " + match[2]; | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| static applyContextMenuTranslation(app) { | |
| // 右键上下文菜单 | |
| var f = LGraphCanvas.prototype.getCanvasMenuOptions; | |
| LGraphCanvas.prototype.getCanvasMenuOptions = function () { | |
| var res = f.apply(this, arguments); | |
| let menuT = TUtils.T.Menu; | |
| for (let item of res) { | |
| if (item == null || !item.hasOwnProperty("content")) continue; | |
| if (item.content in menuT) { | |
| item.content = menuT[item.content]; | |
| } | |
| } | |
| return res; | |
| }; | |
| const f2 = LiteGraph.ContextMenu; | |
| LiteGraph.ContextMenu = function (values, options) { | |
| // 右键上下文菜单先从此处翻译, 随后会经过 applyMenuTranslation走通用翻译 | |
| if (options.hasOwnProperty("title") && options.title in TUtils.T.Nodes) { | |
| options.title = TUtils.T.Nodes[options.title]["title"] || options.title; | |
| } | |
| // Convert {w.name} to input | |
| // Convert {w.name} to widget | |
| var t = TUtils.T.Menu; | |
| var tN = TUtils.T.Nodes; | |
| var reInput = /Convert (.*) to input/; | |
| var reWidget = /Convert (.*) to widget/; | |
| var cvt = t["Convert "] || "Convert "; | |
| var tinp = t[" to input"] || " to input"; | |
| var twgt = t[" to widget"] || " to widget"; | |
| for (let value of values) { | |
| if (value == null || !value.hasOwnProperty("content")) continue; | |
| // 子菜单先走 节点标题菜单 | |
| if (value.value in tN) | |
| { | |
| value.content = tN[value.value]["title"] || value.content; | |
| continue; | |
| } | |
| // inputs | |
| if (value.content in t) { | |
| value.content = t[value.content]; | |
| continue; | |
| } | |
| var extra_info = options.extra || options.parentMenu?.options?.extra; // for capture translation text of input and widget | |
| // widgets and inputs | |
| var matchInput = value.content?.match(reInput); | |
| if (matchInput) { | |
| var match = matchInput[1]; | |
| extra_info?.inputs?.find((i) => { | |
| if (i.name != match) return false; | |
| match = i.label ? i.label : i.name; | |
| }); | |
| extra_info?.widgets?.find((i) => { | |
| if (i.name != match) return false; | |
| match = i.label ? i.label : i.name; | |
| }); | |
| value.content = cvt + match + tinp; | |
| continue; | |
| } | |
| var matchWidget = value.content?.match(reWidget); | |
| if (matchWidget) { | |
| var match = matchWidget[1]; | |
| extra_info?.inputs?.find((i) => { | |
| if (i.name != match) return false; | |
| match = i.label ? i.label : i.name; | |
| }); | |
| extra_info?.widgets?.find((i) => { | |
| if (i.name != match) return false; | |
| match = i.label ? i.label : i.name; | |
| }); | |
| value.content = cvt + match + twgt; | |
| continue; | |
| } | |
| } | |
| const ctx = f2.call(this, values, options); | |
| return ctx; | |
| }; | |
| LiteGraph.ContextMenu.prototype = f2.prototype; | |
| // search box | |
| // var f3 = LiteGraph.LGraphCanvas.prototype.showSearchBox; | |
| // LiteGraph.LGraphCanvas.prototype.showSearchBox = function (event) { | |
| // var res = f3.apply(this, arguments); | |
| // var t = TUtils.T.Menu; | |
| // var name = this.search_box.querySelector(".name"); | |
| // if (name.innerText in t) | |
| // name.innerText = t[name.innerText]; | |
| // t = TUtils.T.Nodes; | |
| // var helper = this.search_box.querySelector(".helper"); | |
| // var items = helper.getElementsByClassName("litegraph lite-search-item"); | |
| // for (let item of items) { | |
| // if (item.innerText in t) | |
| // item.innerText = t[item.innerText]["title"]; | |
| // } | |
| // return res; | |
| // }; | |
| // LiteGraph.LGraphCanvas.prototype.showSearchBox.prototype = f3.prototype; | |
| } | |
| static addRegisterNodeDefCB(app) { | |
| const f = app.registerNodeDef; | |
| async function af() { | |
| return f.apply(this, arguments); | |
| } | |
| app.registerNodeDef = async function (nodeId, nodeData) { | |
| var res = af.apply(this, arguments); | |
| res.then(() => { | |
| TUtils.applyNodeTypeTranslationEx(nodeId); | |
| }); | |
| return res; | |
| }; | |
| } | |
| static addSettingsMenuOptions(app) { | |
| let id = this.LOCALE_ID; | |
| app.ui.settings.addSetting({ | |
| id: id, | |
| name: "Locale", | |
| type: (name, setter, value) => { | |
| const options = [ | |
| ...Object.entries(TUtils.T.Locales).map((v) => { | |
| let nativeName = v[1].nativeName; | |
| let englishName = ""; | |
| if (v[1].englishName != nativeName) englishName = ` [${v[1].englishName}]`; | |
| return $el("option", { | |
| textContent: v[1].nativeName + englishName, | |
| value: v[0], | |
| selected: v[0] === value, | |
| }); | |
| }), | |
| ]; | |
| TUtils.ELS.select = $el( | |
| "select", | |
| { | |
| style: { | |
| marginBottom: "0.15rem", | |
| width: "100%", | |
| }, | |
| onchange: (e) => { | |
| setter(e.target.value); | |
| }, | |
| }, | |
| options | |
| ); | |
| return $el("tr", [ | |
| $el("td", [ | |
| $el("label", { | |
| for: id.replaceAll(".", "-"), | |
| textContent: "AGLTranslation-langualge", | |
| }), | |
| ]), | |
| $el("td", [ | |
| TUtils.ELS.select, | |
| $el("div", { | |
| style: { | |
| display: "grid", | |
| gap: "4px", | |
| gridAutoFlow: "column", | |
| }, | |
| }), | |
| ]), | |
| ]); | |
| }, | |
| defaultValue: localStorage[id] || "en-US", | |
| async onChange(value) { | |
| if (!value) return; | |
| if (localStorage[id] != undefined && value != localStorage[id]) { | |
| TUtils.setLocale(value); | |
| } | |
| localStorage[id] = value; | |
| }, | |
| }); | |
| } | |
| } | |
| const ext = { | |
| name: "AIGODLIKE.Translation", | |
| async init(app) { | |
| // Any initial setup to run as soon as the page loads | |
| TUtils.enhandeDrawNodeWidgets(); | |
| TUtils.syncTranslation(); | |
| return; | |
| var f = app.graphToPrompt; | |
| app.graphToPrompt = async function () { | |
| var res = await f.apply(this, arguments); | |
| if (res.hasOwnProperty("workflow")) { | |
| for (let node of res.workflow.nodes) { | |
| if (node.inputs == undefined) continue; | |
| if (!(node.type in TRANSLATIONS && TRANSLATIONS[node.type].hasOwnProperty("inputs"))) continue; | |
| for (let input of node.inputs) { | |
| var t_inputs = TRANSLATIONS[node.type]["inputs"]; | |
| for (let name in t_inputs) { | |
| if (input.name == t_inputs[name]) { | |
| input.name = name; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (res.hasOwnProperty("output")) { | |
| for (let oname in res.output) { | |
| let o = res.output[oname]; | |
| if (o.inputs == undefined) continue; | |
| if (!(o.class_type in TRANSLATIONS && TRANSLATIONS[o.class_type].hasOwnProperty("widgets"))) continue; | |
| var t_inputs = TRANSLATIONS[o.class_type]["widgets"]; | |
| var rm_keys = []; | |
| for (let iname in o.inputs) { | |
| for (let name in t_inputs) { | |
| if (iname == name) | |
| // 没有翻译的不管 | |
| continue; | |
| if (iname == t_inputs[name]) { | |
| o.inputs[name] = o.inputs[iname]; | |
| rm_keys.push(iname); | |
| } | |
| } | |
| } | |
| for (let rm_key of rm_keys) { | |
| delete o.inputs[rm_key]; | |
| } | |
| } | |
| } | |
| return res; | |
| }; | |
| }, | |
| async setup(app) { | |
| TUtils.applyNodeTypeTranslation(app); | |
| TUtils.applyContextMenuTranslation(app); | |
| TUtils.applyMenuTranslation(app); | |
| TUtils.addRegisterNodeDefCB(app); | |
| TUtils.addSettingsMenuOptions(app); | |
| // 构造设置面板 | |
| // this.settings = new AGLSettingsDialog(); | |
| // 添加按钮 | |
| app.ui.menuContainer.appendChild( | |
| $el("button.agl-swlocale-btn", { | |
| id: "swlocale-button", | |
| textContent: TUtils.T.Menu["Switch Locale"] || "Switch Locale", | |
| onclick: () => { | |
| var localeLast = localStorage.getItem(TUtils.LOCALE_ID_LAST) || "en-US"; | |
| var locale = localStorage.getItem(TUtils.LOCALE_ID) || "en-US"; | |
| if (locale != "en-US" && localeLast != "en-US") localeLast = "en-US"; | |
| if (locale != localeLast) { | |
| app.ui.settings.setSettingValue(TUtils.LOCALE_ID, localeLast); | |
| } | |
| }, | |
| }) | |
| ); | |
| }, | |
| async addCustomNodeDefs(defs, app) { | |
| // Add custom node definitions | |
| // These definitions will be configured and registered automatically | |
| // defs is a lookup core nodes, add yours into this | |
| // console.log("[logging]", "add custom node definitions", "current nodes:", Object.keys(defs)); | |
| }, | |
| async getCustomWidgets(app) { | |
| // Return custom widget types | |
| // See ComfyWidgets for widget examples | |
| // console.log("[logging]", "provide custom widgets"); | |
| }, | |
| async beforeRegisterNodeDef(nodeType, nodeData, app) { | |
| TUtils.applyNodeDescTranslation(nodeType, nodeData, app); | |
| // Run custom logic before a node definition is registered with the graph | |
| // console.log("[logging]", "before register node: ", nodeType.comfyClass); | |
| // This fires for every node definition so only log once | |
| // applyNodeTranslationDef(nodeType, nodeData); | |
| // delete ext.beforeRegisterNodeDef; | |
| }, | |
| async registerCustomNodes(app) { | |
| // Register any custom node implementations here allowing for more flexability than a custom node def | |
| // console.log("[logging]", "register custom nodes"); | |
| }, | |
| loadedGraphNode(node, app) { | |
| // Fires for each node when loading/dragging/etc a workflow json or png | |
| // If you break something in the backend and want to patch workflows in the frontend | |
| // This fires for every node on each load so only log once | |
| // delete ext.loadedGraphNode; | |
| TUtils.applyNodeTranslation(node); | |
| }, | |
| nodeCreated(node, app) { | |
| // Fires every time a node is constructed | |
| // You can modify widgets/add handlers/etc here | |
| TUtils.applyNodeTranslation(node); | |
| }, | |
| }; | |
| app.registerExtension(ext); | |