feat: 添加多语言支持(中英文)

- 添加 i18n 模块,检测 ComfyUI 语言设置
- 创建 locales/zh/main.json 和 locales/en/main.json
- 替换所有硬编码的中文文本为 i18n.t() 调用
- 支持右键菜单、Photopea 模态框、状态提示等文本
This commit is contained in:
InpaintRegionEditor Dev
2026-03-03 20:06:34 +08:00
parent f71183b39e
commit 3268b79853
3 changed files with 173 additions and 36 deletions
+41
View File
@@ -0,0 +1,41 @@
{
"menu": {
"editImage": "Edit Image (Photopea)",
"editMask": "Edit Mask (Photopea)",
"openMaskEditor": "Open in MaskEditor",
"selectImageFirst": "Please select an image first",
"imageNotLoaded": "Image not loaded yet"
},
"photopea": {
"editHint": "Click Save after editing",
"save": "Save",
"cancel": "Cancel",
"loading": "Loading...",
"loaded": "Loaded",
"exporting": "Exporting...",
"uploading": "Uploading...",
"saved": "Saved",
"error": "Error: "
},
"maskEditor": {
"hint": "Draw with black brush for mask, gray for feathered edges",
"saveMask": "Save Mask",
"loadRgb": "Loading RGB data...",
"loadAlpha": "Loading Alpha channel...",
"prepareMask": "Preparing mask layer...",
"converting": "Converting format...",
"loadImageLayer": "Loading image layer...",
"loadMaskLayer": "Loading mask layer...",
"maskLayerName": "Mask Layer (edit this)",
"referenceImage": "Reference Image",
"ready": "Ready - Draw on mask layer",
"prepareEncoder": "Preparing encoder...",
"exportMask": "Exporting mask...",
"processMask": "Processing mask...",
"noMaskData": "No mask data received",
"maskSaved": "Mask saved"
},
"node": {
"referenceArea": "Region"
}
}
+41
View File
@@ -0,0 +1,41 @@
{
"menu": {
"editImage": "编辑图像(Photopea)",
"editMask": "编辑蒙版(Photopea)",
"openMaskEditor": "Open in MaskEditor",
"selectImageFirst": "请先选择图像",
"imageNotLoaded": "图像尚未加载完成"
},
"photopea": {
"editHint": "编辑后点击保存",
"save": "保存",
"cancel": "取消",
"loading": "加载中...",
"loaded": "已加载",
"exporting": "导出中...",
"uploading": "上传中...",
"saved": "已保存",
"error": "错误: "
},
"maskEditor": {
"hint": "用黑色画笔绘制遮罩区域,灰色可产生羽化边缘",
"saveMask": "保存蒙版",
"loadRgb": "加载 RGB 数据...",
"loadAlpha": "加载 Alpha 通道...",
"prepareMask": "准备蒙版层...",
"converting": "转换图像格式...",
"loadImageLayer": "加载图像层...",
"loadMaskLayer": "加载蒙版层...",
"maskLayerName": "蒙版层(编辑此层)",
"referenceImage": "参考图像",
"ready": "就绪 - 请在蒙版层上绘制",
"prepareEncoder": "准备编码器...",
"exportMask": "导出蒙版...",
"processMask": "处理蒙版...",
"noMaskData": "未获取到蒙版数据",
"maskSaved": "蒙版已保存"
},
"node": {
"referenceArea": "参考区"
}
}
+91 -36
View File
@@ -7,7 +7,57 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// 动态加载 UPNG.js 和 pako(用于保留 Alpha=0 时的 RGB 数据)
// ==================== i18n 多语言支持 ====================
const i18n = {
data: {},
// 获取当前语言
getLocale() {
return localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US';
},
// 是否是中文
isZh() {
const locale = this.getLocale();
return locale.startsWith('zh');
},
// 加载语言文件
async load() {
const locale = this.getLocale();
const lang = locale.startsWith('zh') ? 'zh' : 'en';
try {
const resp = await fetch(`/extensions/InpaintRegionEditor/locales/${lang}/main.json`);
if (resp.ok) {
this.data = await resp.json();
}
} catch (e) {
console.warn('Failed to load locale:', e);
}
},
// 获取翻译
t(key) {
const keys = key.split('.');
let value = this.data;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return key; // 找不到返回 key
}
}
return typeof value === 'string' ? value : key;
}
};
// 快捷函数
function t(key) {
return i18n.t(key);
}
// ==================== UPNG.js 动态加载 ====================
let UPNG = null;
async function loadUPNG() {
if (UPNG) return UPNG;
@@ -101,7 +151,7 @@ function showPhotopeaModal(node, imagePath) {
}
ppState.modal = document.createElement("div");
ppState.modal.className = "pp-modal";
ppState.modal.innerHTML = `<div class="pp-container"><iframe id="pp-iframe" src="https://www.photopea.com/"></iframe><div class="pp-toolbar"><span class="pp-hint">编辑后点击保存</span><div><button id="pp-save" class="pp-btn pp-save">保存</button><button id="pp-cancel" class="pp-btn pp-cancel">取消</button></div></div><div class="pp-status" id="pp-status">加载中...</div></div>`;
ppState.modal.innerHTML = `<div class="pp-container"><iframe id="pp-iframe" src="https://www.photopea.com/"></iframe><div class="pp-toolbar"><span class="pp-hint">${t('photopea.editHint')}</span><div><button id="pp-save" class="pp-btn pp-save">${t('photopea.save')}</button><button id="pp-cancel" class="pp-btn pp-cancel">${t('photopea.cancel')}</button></div></div><div class="pp-status" id="pp-status">${t('photopea.loading')}</div></div>`;
document.body.appendChild(ppState.modal);
document.getElementById("pp-save").onclick = saveImg;
document.getElementById("pp-cancel").onclick = closePhotopeaModal;
@@ -122,16 +172,16 @@ async function loadImg() {
const resp = await fetch(url);
if (!resp.ok) throw new Error("HTTP " + resp.status);
await PhotopeaBridge.openImage(await resp.blob());
setStatus("已加载");
} catch (e) { setStatus("错误: " + e.message); }
setStatus(t('photopea.loaded'));
} catch (e) { setStatus(t('photopea.error') + e.message); }
}
async function saveImg() {
if (!ppState.open) return;
try {
setStatus("导出中...");
setStatus(t('photopea.exporting'));
const blob = await PhotopeaBridge.exportImage("png");
setStatus("上传中...");
setStatus(t('photopea.uploading'));
const fd = new FormData();
fd.append("image", blob, "edited_" + Date.now() + ".png");
fd.append("type", "input");
@@ -141,10 +191,10 @@ async function saveImg() {
const w = ppState.node.widgets?.find(x => x.name === "image");
if (w) { w.value = result.name; if (w.callback) w.callback(result.name); }
ppState.node.setDirtyCanvas(true);
setStatus("已保存");
setStatus(t('photopea.saved'));
setTimeout(closePhotopeaModal, 500);
}
} catch (e) { setStatus("错误: " + e.message); }
} catch (e) { setStatus(t('photopea.error') + e.message); }
}
function closePhotopeaModal() {
@@ -169,7 +219,7 @@ function showPhotopeaMaskModal(node, imagePath) {
ppMaskState.modal = document.createElement("div");
ppMaskState.modal.className = "pp-modal";
ppMaskState.modal.innerHTML = `<div class="pp-container"><iframe id="pp-iframe" src="https://www.photopea.com/"></iframe><div class="pp-toolbar"><span class="pp-hint">用黑色画笔绘制遮罩区域,灰色可产生羽化边缘</span><div><button id="pp-save" class="pp-btn pp-save">保存蒙版</button><button id="pp-cancel" class="pp-btn pp-cancel">取消</button></div></div><div class="pp-status" id="pp-status">加载中...</div></div>`;
ppMaskState.modal.innerHTML = `<div class="pp-container"><iframe id="pp-iframe" src="https://www.photopea.com/"></iframe><div class="pp-toolbar"><span class="pp-hint">${t('maskEditor.hint')}</span><div><button id="pp-save" class="pp-btn pp-save">${t('maskEditor.saveMask')}</button><button id="pp-cancel" class="pp-btn pp-cancel">${t('photopea.cancel')}</button></div></div><div class="pp-status" id="pp-status">${t('photopea.loading')}</div></div>`;
document.body.appendChild(ppMaskState.modal);
document.getElementById("pp-save").onclick = saveMaskImg;
@@ -198,14 +248,14 @@ async function loadImgForMask() {
// 关键修复:分别获取 RGB 和 Alpha,绑过 Canvas 的 premultiplied alpha 问题
// 1. 获取完整 RGB(不会被掏窟窿)
setMaskStatus("加载 RGB 数据...");
setMaskStatus(t('maskEditor.loadRgb'));
let rgbUrl = baseUrl + "&channel=rgb";
const rgbResp = await fetch(rgbUrl);
if (!rgbResp.ok) throw new Error("HTTP " + rgbResp.status);
const rgbBlob = await rgbResp.blob();
// 2. 获取 Alpha 通道(用于创建蒙版层)
setMaskStatus("加载 Alpha 通道...");
setMaskStatus(t('maskEditor.loadAlpha'));
let alphaUrl = baseUrl + "&channel=a";
const alphaResp = await fetch(alphaUrl);
let alphaBlob = null;
@@ -214,11 +264,11 @@ async function loadImgForMask() {
}
// 3. 从 Alpha 通道创建蒙版层
setMaskStatus("准备蒙版层...");
setMaskStatus(t('maskEditor.prepareMask'));
const maskBlob = await createMaskFromAlpha(alphaBlob);
// 4. 转换为 base64
setMaskStatus("转换图像格式...");
setMaskStatus(t('maskEditor.converting'));
const rgbBase64 = await blobToBase64(rgbBlob);
const maskBase64 = await blobToBase64(maskBlob);
@@ -227,14 +277,14 @@ async function loadImgForMask() {
console.log("蒙版 blob 大小:", maskBlob.size);
// 5. 先打开 RGB 图像(完整 RGB,没窟窿)
setMaskStatus("加载图像层...");
setMaskStatus(t('maskEditor.loadImageLayer'));
await PhotopeaBridge.postMessage('app.open("' + rgbBase64 + '", null, false);');
// 6. 等待一下确保文档加载完成
await new Promise(r => setTimeout(r, 500));
// 7. 将蒙版作为新图层粘贴到当前文档
setMaskStatus("加载蒙版层...");
setMaskStatus(t('maskEditor.loadMaskLayer'));
await PhotopeaBridge.postMessage('app.open("' + maskBase64 + '", null, true);');
// 8. 设置图层名称
@@ -243,15 +293,15 @@ async function loadImgForMask() {
(function() {
var doc = app.activeDocument;
if (doc.layers.length >= 2) {
doc.layers[0].name = "蒙版层(编辑此层)";
doc.layers[doc.layers.length - 1].name = "参考图像";
doc.layers[0].name = "${t('maskEditor.maskLayerName')}";
doc.layers[doc.layers.length - 1].name = "${t('maskEditor.referenceImage')}";
}
})();
`);
setMaskStatus("就绪 - 请在蒙版层上绘制");
setMaskStatus(t('maskEditor.ready'));
} catch (e) {
setMaskStatus("错误: " + e.message);
setMaskStatus(t('photopea.error') + e.message);
console.error(e);
}
}
@@ -414,14 +464,14 @@ async function saveMaskImg() {
if (!ppMaskState.open) return;
try {
// 提前加载 UPNG.js
setMaskStatus("准备编码器...");
setMaskStatus(t('maskEditor.prepareEncoder'));
try {
await loadUPNG();
} catch (e) {
console.error("加载 UPNG.js 失败:", e);
}
setMaskStatus("导出蒙版...");
setMaskStatus(t('maskEditor.exportMask'));
// 先检查图层信息
const checkScript = `
@@ -463,9 +513,9 @@ async function saveMaskImg() {
}
}
if (!maskBlob) throw new Error("未获取到蒙版数据");
if (!maskBlob) throw new Error(t('maskEditor.noMaskData'));
setMaskStatus("处理蒙版...");
setMaskStatus(t('maskEditor.processMask'));
// 获取原始图像
const originalImageBlob = await getOriginalImageBlob(ppMaskState.path);
@@ -473,7 +523,7 @@ async function saveMaskImg() {
// 合并蒙版到原图
const finalBlob = await mergeMaskToImage(originalImageBlob, maskBlob);
setMaskStatus("上传中...");
setMaskStatus(t('photopea.uploading'));
const fd = new FormData();
fd.append("image", finalBlob, "mask_" + Date.now() + ".png");
fd.append("type", "input");
@@ -488,11 +538,11 @@ async function saveMaskImg() {
if (w.callback) w.callback(uploadResult.name);
}
ppMaskState.node.setDirtyCanvas(true);
setMaskStatus("蒙版已保存");
setMaskStatus(t('maskEditor.maskSaved'));
setTimeout(closePhotopeaMaskModal, 500);
}
} catch (e) {
setMaskStatus("错误: " + e.message);
setMaskStatus(t('photopea.error') + e.message);
console.error(e);
}
}
@@ -796,9 +846,11 @@ function drawNode(ctx, node) {
// 参考区标签
ctx.font = "8px sans-serif";
ctx.fillStyle = "rgba(255, 165, 0, 0.9)";
ctx.fillRect(rx + 2, ry + 2, 55, 10);
const labelText = t('node.referenceArea') + " " + Math.round(data.regionWidth) + "×" + Math.round(data.regionHeight);
const labelWidth = ctx.measureText(labelText).width + 6;
ctx.fillRect(rx + 2, ry + 2, labelWidth, 10);
ctx.fillStyle = "#000";
ctx.fillText("参考区 " + Math.round(data.regionWidth) + "×" + Math.round(data.regionHeight), rx + 3, ry + 9);
ctx.fillText(labelText, rx + 3, ry + 9);
}
}
@@ -1127,7 +1179,10 @@ function onMouseUp() {
app.registerExtension({
name: "InpaintRegionEditor",
init() {
async init() {
// 初始化多语言
await i18n.load();
// 全局粘贴事件监听(捕获阶段,优先于系统处理)
document.addEventListener("paste", async (e) => {
// 检查是否有选中的节点
@@ -1192,20 +1247,20 @@ app.registerExtension({
const imgW = node.widgets?.find(w => w.name === "image");
options.push(null);
options.push({
content: "编辑图像(Photopea)",
callback: function() { imgW?.value ? showPhotopeaModal(node, imgW.value) : alert("请先选择图像"); }
content: t('menu.editImage'),
callback: function() { imgW?.value ? showPhotopeaModal(node, imgW.value) : alert(t('menu.selectImageFirst')); }
});
options.push({
content: "编辑蒙版(Photopea)",
callback: function() { imgW?.value ? showPhotopeaMaskModal(node, imgW.value) : alert("请先选择图像"); }
content: t('menu.editMask'),
callback: function() { imgW?.value ? showPhotopeaMaskModal(node, imgW.value) : alert(t('menu.selectImageFirst')); }
});
// Open in MaskEditor - 使用正确的命令调用
options.push({
content: "Open in MaskEditor",
content: t('menu.openMaskEditor'),
callback: function() {
if (!imgW?.value) { alert("请先选择图像"); return; }
if (!imgW?.value) { alert(t('menu.selectImageFirst')); return; }
const data = nodeImageData.get(node.id);
if (!data?.imageUrl) { alert("图像尚未加载完成"); return; }
if (!data?.imageUrl) { alert(t('menu.imageNotLoaded')); return; }
// 创建带有正确 URL 的图像对象供 MaskEditor 使用
const maskEditorImg = new Image();