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);