From 6fc152254ec6a50694bc43bae1777fa07df9e1e0 Mon Sep 17 00:00:00 2001
From: Katsuyuki-Karasawa <4ranci0ne@gmail.com>
Date: Sun, 4 Aug 2024 01:06:25 +0900
Subject: [PATCH 1/5] init
---
README.md | 9 +-
README_JA.md | 81 ---
README_ZH.md | 81 ---
javascript/bilingual_localization.js | 510 ++---------------
.../original_bilingual_localization.bak | 536 ++++++++++++++++++
src/config/opts.ts | 9 +
src/init.ts | 169 ++++++
src/lib/check-regax.ts | 18 +
src/lib/create-logger.ts | 68 +++
src/lib/delegate-event.ts | 11 +
src/lib/do-translate.ts | 92 +++
src/lib/get-regax.ts | 18 +
src/lib/gradio-app.ts | 22 +
src/lib/handle-dropdown.ts | 26 +
src/lib/html-encode.ts | 4 +
src/lib/parse-html-string-to-element.ts | 5 +
src/lib/read-files.ts | 7 +
src/lib/tranlate-page.ts | 44 ++
src/lib/translate-el.ts | 48 ++
src/main.ts | 31 +
src/setup.ts | 99 ++++
src/types/opts.d.ts | 8 +
22 files changed, 1256 insertions(+), 640 deletions(-)
delete mode 100644 README_JA.md
delete mode 100644 README_ZH.md
create mode 100644 javascript/original_bilingual_localization.bak
create mode 100644 src/config/opts.ts
create mode 100644 src/init.ts
create mode 100644 src/lib/check-regax.ts
create mode 100644 src/lib/create-logger.ts
create mode 100644 src/lib/delegate-event.ts
create mode 100644 src/lib/do-translate.ts
create mode 100644 src/lib/get-regax.ts
create mode 100644 src/lib/gradio-app.ts
create mode 100644 src/lib/handle-dropdown.ts
create mode 100644 src/lib/html-encode.ts
create mode 100644 src/lib/parse-html-string-to-element.ts
create mode 100644 src/lib/read-files.ts
create mode 100644 src/lib/tranlate-page.ts
create mode 100644 src/lib/translate-el.ts
create mode 100644 src/main.ts
create mode 100644 src/setup.ts
create mode 100644 src/types/opts.d.ts
diff --git a/README.md b/README.md
index abb32b2..f58220e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-[中文文档](README_ZH.md) / [日本語](README_JA.md)
-

# sd-webui-bilingual-localization
@@ -7,6 +5,13 @@

+## Build
+
+```bash
+bun build ./src/main.ts --outfile ./javascript/bilingual_localization.js --minify
+```
+
+
## Features
- Bilingual translation, no need to worry about how to find the original button.
- Compatible with language pack extensions, no need to re-import.
diff --git a/README_JA.md b/README_JA.md
deleted file mode 100644
index 12b5038..0000000
--- a/README_JA.md
+++ /dev/null
@@ -1,81 +0,0 @@
-[English Version](README.md)
-
-
-
-# sd-webui-bilingual-localization
-[Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)のバイリンガル対応拡張機能
-
-
-
-## 特徴
-- バイリンガル対応により、元のボタンを探す必要がありません。
-- 日本語化拡張機能と互換性があり、ファイルを取り込み直す必要はありません。
-- ツールチップの動的翻訳をサポートします。
-- スコープと正規表現パターンによる柔軟な翻訳が可能です。
-
-## インストール
-
-以下の方法から選択します。
-拡張機能に対応したWebUI(2023年以降のバージョン)が必要です。
-
-#### 方法1
-
-WebUIの`Install from URL`でインストールを行います。
-
-Extensions - Install from URLを順にクリックします。
-
-1個目のテキストボックスに`https://github.com/journey-ad/sd-webui-bilingual-localization`を入力し、Installボタンをクリックします。
-
-
-
-その後、Installedパネルに切り替え、Apply and restart UIボタンをクリックします。
-
-
-
-
-#### 方法2
-
-拡張機能のディレクトリに手動でcloneします。
-
-```bash
-git clone https://github.com/journey-ad/sd-webui-bilingual-localization extensions/sd-webui-bilingual-localization
-```
-
-## 使用方法
-
-> **⚠️重要⚠️**
-> Settings - User interface - Localizationが`None`に設定されていることを確認してください。
-
-Settings - Bilingual Localizationパネルで、有効にしたい言語ファイル名を選択し、Apply settingsボタンとReload UIボタンを順にクリックします。
-
-
-
-## スコープ
-
-ローカリゼーションは、グローバルな影響を防ぐためにスコープを限定したサポートを提供します。構文規則は以下の通りです:
-- `####` スコープが指定された要素の祖先のIDと一致する場合にのみ、スコープ付きのテキストが適用されます。
-- `##@##` スコープが指定されたCSSセレクタと一致する場合にのみ、スコープ付きのテキストが適用されます。
-
-```json
- ...
- "##tab_ti##Normal": "正常", // id="tab_ti"の要素の下にある`Normal`のみが`正常`に変換されます
- "##tab_threedopenpose##Normal": "法線マップ", // id="tab_threedopenpose"の要素の下にある`Normal`のみが `法線マップ`に変換されます
- "##@.extra-networks .tab-nav button##Lora": "Loraモデル", // class=".extra-networks .tab-nav button"の要素の下にある`Lora`のみが`Loraモデル`に変換されます
- ...
-```
-
-## 正規表現パターン
-
-正規表現を使った日本語化が可能です。構文ルールは`@@`、キャプチャグループは`$n`です。ドキュメント:[String.prototype.replace()](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replace)。
-```json
-{
- ...
- "@@/^(\\d+) images in this directory, divided into (\\d+) pages$/": "このディレクトリには$1枚の画像、$2ページ",
- "@@/^Favorites path from settings: (.*)$/": "お気に入りのディレクトリパス:$1",
- ...
-}
-```
-
-## 日本語化ファイルの取得
-
-内蔵の日本語化ファイルは提供されなくなりました。サードパーティーの日本語化拡張機能をインストールし、当ページの[使用方法](#使用方法)に記載されている方法でセットアップしてください。
\ No newline at end of file
diff --git a/README_ZH.md b/README_ZH.md
deleted file mode 100644
index 87935d9..0000000
--- a/README_ZH.md
+++ /dev/null
@@ -1,81 +0,0 @@
-[English Version](README.md)
-
-
-
-# sd-webui-bilingual-localization
-[Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 双语对照翻译插件
-
-
-
-## 功能
-- 全新实现的双语对照翻译功能,不必再担心切换翻译后找不到原始功能
-- 兼容原生语言包扩展,无需重新导入多语言语料
-- 支持动态title提示的翻译
-- 额外支持作用域和正则表达式替换,翻译更加灵活
-
-## 安装
-
-以下方式选择其一,需要使用支持扩展功能的 webui (2023年之后的版本)
-
-#### 方式1
-
-使用 webui 提供的`Install from URL`功能安装
-
-按下图所示,依次点击Extensions - Install from URL
-
-然后在第一个文本框内填入`https://github.com/journey-ad/sd-webui-bilingual-localization`,点击Install按钮
-
-
-之后切换到Installed面板,点击Apply and restart UI按钮
-
-
-
-#### 方式2
-
-手动克隆到你的扩展目录里
-
-```bash
-git clone https://github.com/journey-ad/sd-webui-bilingual-localization extensions/sd-webui-bilingual-localization
-```
-
-## 使用
-
-> **⚠️重要⚠️**
-> 确保Settings - User interface - Localization 已设置为了 `None`
-
-在Settings - Bilingual Localization中选择要启用的本地化文件,依次点击Apply settings和Reload UI按钮
-
-
-## 作用域支持
-
-本地化语料支持限定作用域,防止影响全局翻译,语法规则:
-- `####` 仅当节点祖先元素ID匹配指定的作用域时才会生效
-- `##@##` 仅当节点祖先元素匹配指定的CSS选择器时才会生效
-
-```json
-{
- ...
- "##tab_ti##Normal": "正态", // 仅id="tab_ti"元素下的`Normal`会被翻译为`正态`
- "##tab_threedopenpose##Normal": "法线图", // 仅id="tab_threedopenpose"元素下的`Normal`会被翻译为`法线图`
- "##@.extra-networks .tab-nav button##Lora": "Lora模型", // 仅class=".extra-networks .tab-nav button"元素下的`Lora`会被翻译为`Lora模型`
- ...
-}
-```
-
-## 正则表达式支持
-
-本地化语料支持正则表达式替换,语法规则`@@`,括号匹配变量`$n`,参考[String.prototype.replace()](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
-```json
-{
- ...
- "@@/^(\\d+) images in this directory, divided into (\\d+) pages$/": "目录中有$1张图片,共$2页",
- "@@/^Favorites path from settings: (.*)$/": "设置的收藏夹目录:$1",
- ...
-}
-```
-
-## 获取本地化文件
-
-本地化文件不再随插件提供,请安装第三方语言包并按照本文[使用](#使用)部分的方式设置使用
-
-*预览图片中的语言包可以在这里找到 https://gist.github.com/journey-ad/d98ed173321658be6e51f752d6e6163c*
diff --git a/javascript/bilingual_localization.js b/javascript/bilingual_localization.js
index f8c2f12..f0d90da 100644
--- a/javascript/bilingual_localization.js
+++ b/javascript/bilingual_localization.js
@@ -1,15 +1,14 @@
-(function () {
- const customCSS = `
+var m={bilingual_localization_enabled:!0,bilingual_localization_logger:!1,bilingual_localization_file:"None",bilingual_localization_dirs:"{}",bilingual_localization_order:"Translation First"};function w(){const i=new Map,n={badge:!0,label:"Logger",enable:!1};return new Proxy(console,{get:(r,t)=>{if(t==="init")return(f)=>{n.label=f,n.enable=!0};if(!(t in r))return;return(...f)=>{if(!n.enable)return;let a=["#39cfe1","#006cab"],o,c;switch(t){case"error":a=["#f70000","#a70000"];break;case"warn":a=["#f7b500","#b58400"];break;case"time":if(o=f[0],i.has(o))console.warn(`Timer '${o}' already exists`);else i.set(o,performance.now());return;case"timeEnd":if(o=f[0],c=i.get(o),c===void 0)console.warn(`Timer '${o}' does not exist`);else i.delete(o),console.log(`${o}: ${performance.now()-c} ms`);return;case"groupEnd":n.badge=!0;break}const e=n.badge?[`%c${n.label}`,`color: #fff; background: linear-gradient(180deg, ${a[0]}, ${a[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`]:[];if(r[t](...e,...f),t==="group"||t==="groupCollapsed")n.badge=!1}}})}function q(i){let n=new XMLHttpRequest;return n.open("GET",`file=${i}`,!1),n.send(null),n.responseText}function C(i){try{i=i.trim();let n=i.split("/");if(i[0]!=="/"||n.length<3)return i=i.replace(/[.*+\-?^${}()|[\]\\]/g,"\\$&"),new RegExp(i);const r=n[n.length-1],t=i.lastIndexOf("/");return i=i.substring(1,t),new RegExp(i,r)}catch(n){return null}}function D(i,n,r,t){i.addEventListener(n,function(f){var a=f.target;while(a!==i){if(a.matches(r))t.call(a,f);a=a.parentNode}})}function x(){const i=document.getElementsByTagName("gradio-app"),n=i.length===0?document:i[0];if(n!==document)n.getElementById=function(r){return document.getElementById(r)};return n.shadowRoot?n.shadowRoot:n}function _(...i){return x()?.querySelectorAll(...i)||new NodeList}function M(){D(x(),"mousedown","ul.options .item",function(i){const{target:n}=i;if(!n.classList.contains("item")){n.closest(".item").dispatchEvent(new Event("mousedown",{bubbles:!0}));return}const r=n.dataset.value,t=n?.closest(".wrap")?.querySelector(".wrap-inner .single-select");if(r&&t)t.title=titles?.[r]||"",t.textContent="__biligual__will_be_replaced__",p(t,r,"element")})}function u(i,{deep:n=!1,rich:r=!1}={}){if(!l)return;if(i.matches?.(E))return;if(i.title)p(i,i.title,"title");if(i.placeholder)p(i,i.placeholder,"placeholder");if(i.tagName==="OPTION")p(i,i.textContent,"option");if(n||r)Array.from(i.childNodes).forEach((t)=>{if(t.nodeName==="#text"){if(r){p(t,t.textContent,"text");return}if(n)p(t,t.textContent,"element")}else if(t.childNodes.length>0)u(t,{deep:n,rich:r})});else p(i,i.textContent,"element")}var E=[".bilingual__trans_wrapper",".resultsFlexContainer","#setting_sd_model_checkpoint select","#setting_sd_vae select","#txt2img_styles, #img2txt_styles",".extra-network-cards .card .actions .name","script, style, svg, g, path"];function y(){if(!l())return;const i=w();i.time("Full Page");const n=["label span, fieldset span, button","textarea[placeholder], select, option",".transition > div > span:not([class])",".label-wrap > span",".gradio-image>div.float",".gradio-file>div.float",".gradio-code>div.float","#modelmerger_interp_description .output-html","#modelmerger_interp_description .gradio-html","#lightboxModal span"],r=['div[data-testid="image"] > div > div',"#extras_image_batch > div",".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown","#dynamic-prompting"];n.forEach((t)=>{_(t).forEach((f)=>u(f,{deep:!0}))}),r.forEach((t)=>{_(t).forEach((f)=>u(f,{rich:!0}))}),i.timeEnd("Full Page")}function T(){z={enabled:m.bilingual_localization_enabled,file:m.bilingual_localization_file,dirs:m.bilingual_localization_dirs,order:m.bilingual_localization_order,enableLogger:m.bilingual_localization_logger};let{enabled:i,file:n,dirs:r,enableLogger:t}=z;if(!i||n==="None"||r==="None")return;const f=JSON.parse(r),a=w();if(t)a.init("Bilingual");a.log("Bilingual Localization initialized.");const o=/^##(?.+)##(?.+)$/;B=JSON.parse(q(f[n]),(c,e)=>{if(c.startsWith("@@")){const g=C(c.slice(2));if(g instanceof RegExp)O.set(g,e)}else{const g=c.match(o);if(g&&g.groups){let{scope:b,skey:s}=g.groups;if(b.startsWith("@"))b=b.slice(1);else b="#"+b;if(!b.length)return e;R[b]||={},R[b][s]=e,L[s]||=[],L[s].push(b)}else return e}}),y(),M()}function l(){return B}function H(){return O}function S(){return R}function h(){return L}function k(){return z}var B=null,O=new Map,R={},L={},z=null;function v(i){const n=H();for(let[r,t]of n.entries())if(r instanceof RegExp){if(r.test(i))return w().log("regex",r,i,t),i.replace(r,t)}else console.warn("Expected regex to be an instance of RegExp, but it was a string.");return i}function $(i){return i.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function F(i){const n=document.createElement("template");return n.insertAdjacentHTML("afterbegin",i),n.firstElementChild}function p(i,n,r){if(!l)return;if(n=n.trim(),!n)return;if(I.test(n))return;if(J.test(n))return;let t=l[n]||v(n),f=h[n];if(f){console.log("scope",i,n,f);for(let o of f)if(i.parentElement.closest(o)){t=S[o][n];break}}if(!t||n===t){if(i.textContent==="__biligual__will_be_replaced__")i.textContent=n;if(i.nextSibling?.className==="bilingual__trans_wrapper")i.nextSibling.remove();return}if(k()?.order==="Original First")[n,t]=[t,n];switch(r){case"text":i.textContent=t;break;case"element":const o=`${$(t)}${$(n)}
`,c=F(o);if(i.hasChildNodes()){const e=Array.from(i.childNodes).find((g)=>g.nodeType===Node.TEXT_NODE&&g.textContent?.trim()===n||g.textContent?.trim()==="__bilingual__will_be_replaced__");if(e){if(e.textContent="",e.nextSibling?.nodeType===Node.ELEMENT_NODE&&e.nextSibling.className==="bilingual__trans_wrapper")e.nextSibling.remove();if(e.parentNode&&c)e.parentNode.insertBefore(c,e.nextSibling)}}else{if(i.textContent="",i.nextSibling?.nodeType===Node.ELEMENT_NODE&&i.nextSibling.className==="bilingual__trans_wrapper")i.nextSibling.remove();if(i.parentNode&&c)i.parentNode.insertBefore(c,i.nextSibling)}break;case"option":i.textContent=`${t} (${n})`;break;case"title":i.title=`${t}\n${n}`;break;case"placeholder":i.placeholder=`${t}\n\n${n}`;break;default:return t}}var I=/^[\.\d]+$/,J=/[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;function A(){const i=document.createElement("style");if(i.textContent)i.textContent=P;else i.appendChild(document.createTextNode(P));x().appendChild(i);let n=!1,r=0;new MutationObserver((f)=>{if(window.localization&&Object.keys(window.localization).length)return;if(Object.keys(m).length===0)return;let a=0,o=performance.now();for(let e of f)if(e.type==="characterData"){if(e.target?.parentElement?.parentElement?.tagName==="LABEL")u(e.target)}else if(e.type==="attributes")a++,u(e.target);else e.addedNodes.forEach((g)=>{if(g instanceof Element&&g.className==="bilingual__trans_wrapper")return;if(a++,g.nodeType===1&&g instanceof Element&&/(output|gradio)-(html|markdown)/.test(g.className))u(g,{rich:!0});else if(g.nodeType===3)p(g,g.textContent,"text");else u(g,{deep:!0})});if(a>0)w().info(`UI Update #${r++}: ${performance.now()-o} ms, ${a} nodes`,f);if(n)return;if(l())return;n=!0,T()}).observe(x(),{characterData:!0,childList:!0,subtree:!0,attributes:!0,attributeFilter:["title","placeholder"]})}var P=`
.bilingual__trans_wrapper {
- display: inline-flex;
- flex-direction: column;
- align-items: center;
- font-size: 13px;
- line-height: 1;
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 13px;
+ line-height: 1;
}
-
+
.bilingual__trans_wrapper em {
- font-style: normal;
+ font-style: normal;
}
#txtimg_hr_finalres .bilingual__trans_wrapper em,
@@ -20,9 +19,9 @@
#available_extensions .date_added .bilingual__trans_wrapper em,
#available_extensions+p>.bilingual__trans_wrapper em,
.gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
- display: none;
+ display: none;
}
-
+
#settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
label>span>.bilingual__trans_wrapper,
fieldset>span>.bilingual__trans_wrapper,
@@ -40,497 +39,56 @@
.posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
#dynamic-prompting .bilingual__trans_wrapper
{
- font-size: 12px;
- align-items: flex-start;
+ font-size: 12px;
+ align-items: flex-start;
}
#extensions label .bilingual__trans_wrapper,
#available_extensions td .bilingual__trans_wrapper,
.label-wrap>span>.bilingual__trans_wrapper {
- font-size: inherit;
- line-height: inherit;
+ font-size: inherit;
+ line-height: inherit;
}
.label-wrap>span:first-of-type {
- font-size: 13px;
- line-height: 1;
+ font-size: 13px;
+ line-height: 1;
}
#txt2img_script_container > div {
- margin-top: var(--layout-gap, 12px);
+ margin-top: var(--layout-gap, 12px);
}
-
+
textarea::placeholder {
- line-height: 1;
- padding: 4px 0;
+ line-height: 1;
+ padding: 4px 0;
}
-
+
label>span {
- line-height: 1;
+ line-height: 1;
}
-
+
div[data-testid="image"] .start-prompt {
- background-color: rgba(255, 255, 255, .6);
- color: #222;
- transition: opacity .2s ease-in-out;
+ background-color: rgba(255, 255, 255, .6);
+ color: #222;
+ transition: opacity .2s ease-in-out;
}
div[data-testid="image"]:hover .start-prompt {
- opacity: 0;
+ opacity: 0;
}
.label-wrap > span.icon {
- width: 1em;
- height: 1em;
- transform-origin: center center;
+ width: 1em;
+ height: 1em;
+ transform-origin: center center;
}
.gradio-dropdown ul.options li.item {
- padding: 0.3em 0.4em !important;
+ padding: 0.3em 0.4em !important;
}
-
+
/* Posex extension */
.posex_bg {
- white-space: nowrap;
- }
- `
-
- let i18n = null, i18nRegex = new Map(), i18nScope = {}, scopedSource = {}, config = null;
-
- // First load
- function setup() {
- config = {
- enabled: opts["bilingual_localization_enabled"],
- file: opts["bilingual_localization_file"],
- dirs: opts["bilingual_localization_dirs"],
- order: opts["bilingual_localization_order"],
- enableLogger: opts["bilingual_localization_logger"]
- }
-
- let { enabled, file, dirs, enableLogger } = config
-
- if (!enabled || file === "None" || dirs === "None") return
-
- dirs = JSON.parse(dirs)
-
- enableLogger && logger.init('Bilingual')
- logger.log('Bilingual Localization initialized.')
-
- // Load localization file
- const regex_scope = /^##(?.+)##(?.+)$/ // ##scope##.skey
- i18n = JSON.parse(readFile(dirs[file]), (key, value) => {
- // parse regex translations
- if (key.startsWith('@@')) {
- const regex = getRegex(key.slice(2))
- if (regex instanceof RegExp) {
- i18nRegex.set(regex, value)
- }
- } else if (regex_scope.test(key)) {
- // parse scope translations
- let { scope, skey } = key.match(regex_scope).groups
-
- if (scope.startsWith('@')) {
- scope = scope.slice(1)
- } else {
- scope = '#' + scope
- }
-
- if (!scope.length) {
- return value
- }
-
- i18nScope[scope] ||= {}
- i18nScope[scope][skey] = value
-
- scopedSource[skey] ||= []
- scopedSource[skey].push(scope)
- } else {
- return value
- }
- })
-
- logger.group('Localization file loaded.')
- logger.log('i18n', i18n)
- logger.log('i18nRegex', i18nRegex)
- logger.log('i18nScope', i18nScope)
- logger.groupEnd()
-
- translatePage()
- handleDropdown()
- }
-
- function handleDropdown() {
- // process gradio dropdown menu
- delegateEvent(gradioApp(), 'mousedown', 'ul.options .item', function (event) {
- const { target } = event
-
- if (!target.classList.contains('item')) {
- // simulate click menu item
- target.closest('.item').dispatchEvent(new Event('mousedown', { bubbles: true }))
- return
- }
-
- const source = target.dataset.value
- const $labelEl = target?.closest('.wrap')?.querySelector('.wrap-inner .single-select') // the label element
-
- if (source && $labelEl) {
- $labelEl.title = titles?.[source] || '' // set title from hints.js
- $labelEl.textContent = "__biligual__will_be_replaced__" // marked as will be replaced
- doTranslate($labelEl, source, 'element') // translate the label element
- }
- });
- }
-
- // Translate page
- function translatePage() {
- if (!i18n) return
-
- logger.time('Full Page')
- querySelectorAll([
- "label span, fieldset span, button", // major label and button description text
- "textarea[placeholder], select, option", // text box placeholder and select element
- ".transition > div > span:not([class])", ".label-wrap > span", // collapse panel added by extension
- ".gradio-image>div.float", // image upload description
- ".gradio-file>div.float", // file upload description
- ".gradio-code>div.float", // code editor description
- "#modelmerger_interp_description .output-html", // model merger description
- "#modelmerger_interp_description .gradio-html", // model merger description
- "#lightboxModal span" // image preview lightbox
- ])
- .forEach(el => translateEl(el, { deep: true }))
-
- querySelectorAll([
- 'div[data-testid="image"] > div > div', // description of image upload panel
- '#extras_image_batch > div', // description of extras image batch file upload panel
- ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", // output html exclude footer
- '#dynamic-prompting' // dynamic-prompting extension
- ])
- .forEach(el => translateEl(el, { rich: true }))
-
- logger.timeEnd('Full Page')
- }
-
- const ignore_selector = [
- '.bilingual__trans_wrapper', // self
- '.resultsFlexContainer', // tag-autocomplete
- '#setting_sd_model_checkpoint select', // model checkpoint
- '#setting_sd_vae select', // vae model
- '#txt2img_styles, #img2txt_styles', // styles select
- '.extra-network-cards .card .actions .name', // extra network cards name
- 'script, style, svg, g, path', // script / style / svg elements
- ]
- // Translate element
- function translateEl(el, { deep = false, rich = false } = {}) {
- if (!i18n) return // translation not ready.
- if (el.matches?.(ignore_selector)) return // ignore some elements.
-
- if (el.title) {
- doTranslate(el, el.title, 'title')
- }
-
- if (el.placeholder) {
- doTranslate(el, el.placeholder, 'placeholder')
- }
-
- if (el.tagName === 'OPTION') {
- doTranslate(el, el.textContent, 'option')
- }
-
- if (deep || rich) {
- Array.from(el.childNodes).forEach(node => {
- if (node.nodeName === '#text') {
- if (rich) {
- doTranslate(node, node.textContent, 'text')
- return
- }
-
- if (deep) {
- doTranslate(node, node.textContent, 'element')
- }
- } else if (node.childNodes.length > 0) {
- translateEl(node, { deep, rich })
- }
- })
- } else {
- doTranslate(el, el.textContent, 'element')
- }
- }
-
- function checkRegex(source) {
- for (const [regex, value] of i18nRegex.entries()) {
- if (regex.test(source)) {
- logger.log('regex', regex, source, value)
- return source.replace(regex, value)
- }
- }
- }
-
- const re_num = /^[\.\d]+$/,
- re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
-
- function doTranslate(el, source, type) {
- if (!i18n) return // translation not ready.
- source = source.trim()
- if (!source) return
- if (re_num.test(source)) return
- // if (re_emoji.test(source)) return
-
- let translation = i18n[source] || checkRegex(source),
- scopes = scopedSource[source]
-
- if (scopes) {
- console.log('scope', el, source, scopes);
- for (let scope of scopes) {
- if (el.parentElement.closest(scope)) {
- translation = i18nScope[scope][source]
- break
- }
- }
+ white-space: nowrap;
}
-
- if (!translation || source === translation) {
- if (el.textContent === '__biligual__will_be_replaced__') el.textContent = source // restore original text if translation not exist
- if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() // remove exist translation if translation not exist
- return
- }
-
- if (config.order === "Original First") {
- [source, translation] = [translation, source]
- }
-
- switch (type) {
- case 'text':
- el.textContent = translation
- break;
-
- case 'element':
- const htmlStr = `${htmlEncode(translation)}${htmlEncode(source)}
`
- const htmlEl = parseHtmlStringToElement(htmlStr)
- if (el.hasChildNodes()) {
- const textNode = Array.from(el.childNodes).find(node =>
- node.nodeName === '#text' &&
- (node.textContent.trim() === source || node.textContent.trim() === '__biligual__will_be_replaced__')
- )
-
- if (textNode) {
- textNode.textContent = ''
- if (textNode.nextSibling?.className === 'bilingual__trans_wrapper') textNode.nextSibling.remove()
- textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling)
- }
- } else {
- el.textContent = ''
- if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove()
- el.parentNode.insertBefore(htmlEl, el.nextSibling)
- }
- break;
-
- case 'option':
- el.textContent = `${translation} (${source})`
- break;
-
- case 'title':
- el.title = `${translation}\n${source}`
- break;
-
- case 'placeholder':
- el.placeholder = `${translation}\n\n${source}`
- break;
-
- default:
- return translation
- }
- }
-
- function gradioApp() {
- const elems = document.getElementsByTagName('gradio-app')
- const elem = elems.length == 0 ? document : elems[0]
-
- if (elem !== document) elem.getElementById = function (id) { return document.getElementById(id) }
- return elem.shadowRoot ? elem.shadowRoot : elem
- }
-
- function querySelector(...args) {
- return gradioApp()?.querySelector(...args)
- }
-
- function querySelectorAll(...args) {
- return gradioApp()?.querySelectorAll(...args)
- }
-
- function delegateEvent(parent, eventType, selector, handler) {
- parent.addEventListener(eventType, function (event) {
- var target = event.target;
- while (target !== parent) {
- if (target.matches(selector)) {
- handler.call(target, event);
- }
- target = target.parentNode;
- }
- });
- }
-
- function parseHtmlStringToElement(htmlStr) {
- const template = document.createElement('template')
- template.insertAdjacentHTML('afterbegin', htmlStr)
- return template.firstElementChild
- }
-
- function htmlEncode(htmlStr) {
- return htmlStr.replace(/&/g, '&').replace(//g, '>')
- .replace(/"/g, '"').replace(/'/g, ''')
- }
-
- // get regex object from string
- function getRegex(regex) {
- try {
- regex = regex.trim();
- let parts = regex.split('/');
- if (regex[0] !== '/' || parts.length < 3) {
- regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string
- return new RegExp(regex);
- }
-
- const option = parts[parts.length - 1];
- const lastIndex = regex.lastIndexOf('/');
- regex = regex.substring(1, lastIndex);
- return new RegExp(regex, option);
- } catch (e) {
- return null
- }
- }
-
- // Load file
- function readFile(filePath) {
- let request = new XMLHttpRequest();
- request.open("GET", `file=${filePath}`, false);
- request.send(null);
- return request.responseText;
- }
-
- const logger = (function () {
- const loggerTimerMap = new Map()
- const loggerConf = { badge: true, label: 'Logger', enable: false }
- return new Proxy(console, {
- get: (target, propKey) => {
- if (propKey === 'init') {
- return (label) => {
- loggerConf.label = label
- loggerConf.enable = true
- }
- }
-
- if (!(propKey in target)) return undefined
-
- return (...args) => {
- if (!loggerConf.enable) return
-
- let color = ['#39cfe1', '#006cab']
-
- let label, start
- switch (propKey) {
- case 'error':
- color = ['#f70000', '#a70000']
- break;
- case 'warn':
- color = ['#f7b500', '#b58400']
- break;
- case 'time':
- label = args[0]
- if (loggerTimerMap.has(label)) {
- logger.warn(`Timer '${label}' already exisits`)
- } else {
- loggerTimerMap.set(label, performance.now())
- }
- return
- case 'timeEnd':
- label = args[0], start = loggerTimerMap.get(label)
- if (start === undefined) {
- logger.warn(`Timer '${label}' does not exist`)
- } else {
- loggerTimerMap.delete(label)
- logger.log(`${label}: ${performance.now() - start} ms`)
- }
- return
- case 'groupEnd':
- loggerConf.badge = true
- break
- }
-
- const badge = loggerConf.badge ? [`%c${loggerConf.label}`, `color: #fff; background: linear-gradient(180deg, ${color[0]}, ${color[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`] : []
-
- target[propKey](...badge, ...args)
-
- if (propKey === 'group' || propKey === 'groupCollapsed') {
- loggerConf.badge = false
- }
- }
- }
- })
- }())
-
- function init() {
- // Add style to dom
- let $styleEL = document.createElement('style');
-
- if ($styleEL.styleSheet) {
- $styleEL.styleSheet.cssText = customCSS;
- } else {
- $styleEL.appendChild(document.createTextNode(customCSS));
- }
- gradioApp().appendChild($styleEL);
-
- let loaded = false
- let _count = 0
-
- const observer = new MutationObserver(mutations => {
- if (window.localization && Object.keys(window.localization).length) return; // disabled if original translation enabled
- if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
-
- let _nodesCount = 0, _now = performance.now()
-
- for (const mutation of mutations) {
- if (mutation.type === 'characterData') {
- if (mutation.target?.parentElement?.parentElement?.tagName === 'LABEL') {
- translateEl(mutation.target)
- }
- } else if (mutation.type === 'attributes') {
- _nodesCount++
- translateEl(mutation.target)
- } else {
- mutation.addedNodes.forEach(node => {
- if (node.className === 'bilingual__trans_wrapper') return
-
- _nodesCount++
- if (node.nodeType === 1 && /(output|gradio)-(html|markdown)/.test(node.className)) {
- translateEl(node, { rich: true })
- } else if (node.nodeType === 3) {
- doTranslate(node, node.textContent, 'text')
- } else {
- translateEl(node, { deep: true })
- }
- })
- }
- }
-
- if (_nodesCount > 0) {
- logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations)
- }
-
- if (loaded) return;
- if (i18n) return;
-
- loaded = true
- setup()
- })
-
- observer.observe(gradioApp(), {
- characterData: true,
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['title', 'placeholder']
- })
- }
-
- // Init after page loaded
- document.addEventListener('DOMContentLoaded', init)
-})();
+ `;document.addEventListener("DOMContentLoaded",()=>{A()});
diff --git a/javascript/original_bilingual_localization.bak b/javascript/original_bilingual_localization.bak
new file mode 100644
index 0000000..f8c2f12
--- /dev/null
+++ b/javascript/original_bilingual_localization.bak
@@ -0,0 +1,536 @@
+(function () {
+ const customCSS = `
+ .bilingual__trans_wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ .bilingual__trans_wrapper em {
+ font-style: normal;
+ }
+
+ #txtimg_hr_finalres .bilingual__trans_wrapper em,
+ #tab_ti .output-html .bilingual__trans_wrapper em,
+ #tab_ti .gradio-html .bilingual__trans_wrapper em,
+ #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
+ #available_extensions .extension-tag .bilingual__trans_wrapper em,
+ #available_extensions .date_added .bilingual__trans_wrapper em,
+ #available_extensions+p>.bilingual__trans_wrapper em,
+ .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
+ display: none;
+ }
+
+ #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
+ label>span>.bilingual__trans_wrapper,
+ fieldset>span>.bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper,
+ .w-full>span>.bilingual__trans_wrapper,
+ .context-menu-items .bilingual__trans_wrapper,
+ .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
+ .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
+ .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
+ .output-markdown .bilingual__trans_wrapper,
+ .gradio-markdown .bilingual__trans_wrapper,
+ .gradio-image>div.float .bilingual__trans_wrapper,
+ .gradio-file>div.float .bilingual__trans_wrapper,
+ .gradio-code>div.float .bilingual__trans_wrapper,
+ .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
+ #dynamic-prompting .bilingual__trans_wrapper
+ {
+ font-size: 12px;
+ align-items: flex-start;
+ }
+
+ #extensions label .bilingual__trans_wrapper,
+ #available_extensions td .bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper {
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ .label-wrap>span:first-of-type {
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ #txt2img_script_container > div {
+ margin-top: var(--layout-gap, 12px);
+ }
+
+ textarea::placeholder {
+ line-height: 1;
+ padding: 4px 0;
+ }
+
+ label>span {
+ line-height: 1;
+ }
+
+ div[data-testid="image"] .start-prompt {
+ background-color: rgba(255, 255, 255, .6);
+ color: #222;
+ transition: opacity .2s ease-in-out;
+ }
+ div[data-testid="image"]:hover .start-prompt {
+ opacity: 0;
+ }
+
+ .label-wrap > span.icon {
+ width: 1em;
+ height: 1em;
+ transform-origin: center center;
+ }
+
+ .gradio-dropdown ul.options li.item {
+ padding: 0.3em 0.4em !important;
+ }
+
+ /* Posex extension */
+ .posex_bg {
+ white-space: nowrap;
+ }
+ `
+
+ let i18n = null, i18nRegex = new Map(), i18nScope = {}, scopedSource = {}, config = null;
+
+ // First load
+ function setup() {
+ config = {
+ enabled: opts["bilingual_localization_enabled"],
+ file: opts["bilingual_localization_file"],
+ dirs: opts["bilingual_localization_dirs"],
+ order: opts["bilingual_localization_order"],
+ enableLogger: opts["bilingual_localization_logger"]
+ }
+
+ let { enabled, file, dirs, enableLogger } = config
+
+ if (!enabled || file === "None" || dirs === "None") return
+
+ dirs = JSON.parse(dirs)
+
+ enableLogger && logger.init('Bilingual')
+ logger.log('Bilingual Localization initialized.')
+
+ // Load localization file
+ const regex_scope = /^##(?.+)##(?.+)$/ // ##scope##.skey
+ i18n = JSON.parse(readFile(dirs[file]), (key, value) => {
+ // parse regex translations
+ if (key.startsWith('@@')) {
+ const regex = getRegex(key.slice(2))
+ if (regex instanceof RegExp) {
+ i18nRegex.set(regex, value)
+ }
+ } else if (regex_scope.test(key)) {
+ // parse scope translations
+ let { scope, skey } = key.match(regex_scope).groups
+
+ if (scope.startsWith('@')) {
+ scope = scope.slice(1)
+ } else {
+ scope = '#' + scope
+ }
+
+ if (!scope.length) {
+ return value
+ }
+
+ i18nScope[scope] ||= {}
+ i18nScope[scope][skey] = value
+
+ scopedSource[skey] ||= []
+ scopedSource[skey].push(scope)
+ } else {
+ return value
+ }
+ })
+
+ logger.group('Localization file loaded.')
+ logger.log('i18n', i18n)
+ logger.log('i18nRegex', i18nRegex)
+ logger.log('i18nScope', i18nScope)
+ logger.groupEnd()
+
+ translatePage()
+ handleDropdown()
+ }
+
+ function handleDropdown() {
+ // process gradio dropdown menu
+ delegateEvent(gradioApp(), 'mousedown', 'ul.options .item', function (event) {
+ const { target } = event
+
+ if (!target.classList.contains('item')) {
+ // simulate click menu item
+ target.closest('.item').dispatchEvent(new Event('mousedown', { bubbles: true }))
+ return
+ }
+
+ const source = target.dataset.value
+ const $labelEl = target?.closest('.wrap')?.querySelector('.wrap-inner .single-select') // the label element
+
+ if (source && $labelEl) {
+ $labelEl.title = titles?.[source] || '' // set title from hints.js
+ $labelEl.textContent = "__biligual__will_be_replaced__" // marked as will be replaced
+ doTranslate($labelEl, source, 'element') // translate the label element
+ }
+ });
+ }
+
+ // Translate page
+ function translatePage() {
+ if (!i18n) return
+
+ logger.time('Full Page')
+ querySelectorAll([
+ "label span, fieldset span, button", // major label and button description text
+ "textarea[placeholder], select, option", // text box placeholder and select element
+ ".transition > div > span:not([class])", ".label-wrap > span", // collapse panel added by extension
+ ".gradio-image>div.float", // image upload description
+ ".gradio-file>div.float", // file upload description
+ ".gradio-code>div.float", // code editor description
+ "#modelmerger_interp_description .output-html", // model merger description
+ "#modelmerger_interp_description .gradio-html", // model merger description
+ "#lightboxModal span" // image preview lightbox
+ ])
+ .forEach(el => translateEl(el, { deep: true }))
+
+ querySelectorAll([
+ 'div[data-testid="image"] > div > div', // description of image upload panel
+ '#extras_image_batch > div', // description of extras image batch file upload panel
+ ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", // output html exclude footer
+ '#dynamic-prompting' // dynamic-prompting extension
+ ])
+ .forEach(el => translateEl(el, { rich: true }))
+
+ logger.timeEnd('Full Page')
+ }
+
+ const ignore_selector = [
+ '.bilingual__trans_wrapper', // self
+ '.resultsFlexContainer', // tag-autocomplete
+ '#setting_sd_model_checkpoint select', // model checkpoint
+ '#setting_sd_vae select', // vae model
+ '#txt2img_styles, #img2txt_styles', // styles select
+ '.extra-network-cards .card .actions .name', // extra network cards name
+ 'script, style, svg, g, path', // script / style / svg elements
+ ]
+ // Translate element
+ function translateEl(el, { deep = false, rich = false } = {}) {
+ if (!i18n) return // translation not ready.
+ if (el.matches?.(ignore_selector)) return // ignore some elements.
+
+ if (el.title) {
+ doTranslate(el, el.title, 'title')
+ }
+
+ if (el.placeholder) {
+ doTranslate(el, el.placeholder, 'placeholder')
+ }
+
+ if (el.tagName === 'OPTION') {
+ doTranslate(el, el.textContent, 'option')
+ }
+
+ if (deep || rich) {
+ Array.from(el.childNodes).forEach(node => {
+ if (node.nodeName === '#text') {
+ if (rich) {
+ doTranslate(node, node.textContent, 'text')
+ return
+ }
+
+ if (deep) {
+ doTranslate(node, node.textContent, 'element')
+ }
+ } else if (node.childNodes.length > 0) {
+ translateEl(node, { deep, rich })
+ }
+ })
+ } else {
+ doTranslate(el, el.textContent, 'element')
+ }
+ }
+
+ function checkRegex(source) {
+ for (const [regex, value] of i18nRegex.entries()) {
+ if (regex.test(source)) {
+ logger.log('regex', regex, source, value)
+ return source.replace(regex, value)
+ }
+ }
+ }
+
+ const re_num = /^[\.\d]+$/,
+ re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
+
+ function doTranslate(el, source, type) {
+ if (!i18n) return // translation not ready.
+ source = source.trim()
+ if (!source) return
+ if (re_num.test(source)) return
+ // if (re_emoji.test(source)) return
+
+ let translation = i18n[source] || checkRegex(source),
+ scopes = scopedSource[source]
+
+ if (scopes) {
+ console.log('scope', el, source, scopes);
+ for (let scope of scopes) {
+ if (el.parentElement.closest(scope)) {
+ translation = i18nScope[scope][source]
+ break
+ }
+ }
+ }
+
+ if (!translation || source === translation) {
+ if (el.textContent === '__biligual__will_be_replaced__') el.textContent = source // restore original text if translation not exist
+ if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() // remove exist translation if translation not exist
+ return
+ }
+
+ if (config.order === "Original First") {
+ [source, translation] = [translation, source]
+ }
+
+ switch (type) {
+ case 'text':
+ el.textContent = translation
+ break;
+
+ case 'element':
+ const htmlStr = `${htmlEncode(translation)}${htmlEncode(source)}
`
+ const htmlEl = parseHtmlStringToElement(htmlStr)
+ if (el.hasChildNodes()) {
+ const textNode = Array.from(el.childNodes).find(node =>
+ node.nodeName === '#text' &&
+ (node.textContent.trim() === source || node.textContent.trim() === '__biligual__will_be_replaced__')
+ )
+
+ if (textNode) {
+ textNode.textContent = ''
+ if (textNode.nextSibling?.className === 'bilingual__trans_wrapper') textNode.nextSibling.remove()
+ textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling)
+ }
+ } else {
+ el.textContent = ''
+ if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove()
+ el.parentNode.insertBefore(htmlEl, el.nextSibling)
+ }
+ break;
+
+ case 'option':
+ el.textContent = `${translation} (${source})`
+ break;
+
+ case 'title':
+ el.title = `${translation}\n${source}`
+ break;
+
+ case 'placeholder':
+ el.placeholder = `${translation}\n\n${source}`
+ break;
+
+ default:
+ return translation
+ }
+ }
+
+ function gradioApp() {
+ const elems = document.getElementsByTagName('gradio-app')
+ const elem = elems.length == 0 ? document : elems[0]
+
+ if (elem !== document) elem.getElementById = function (id) { return document.getElementById(id) }
+ return elem.shadowRoot ? elem.shadowRoot : elem
+ }
+
+ function querySelector(...args) {
+ return gradioApp()?.querySelector(...args)
+ }
+
+ function querySelectorAll(...args) {
+ return gradioApp()?.querySelectorAll(...args)
+ }
+
+ function delegateEvent(parent, eventType, selector, handler) {
+ parent.addEventListener(eventType, function (event) {
+ var target = event.target;
+ while (target !== parent) {
+ if (target.matches(selector)) {
+ handler.call(target, event);
+ }
+ target = target.parentNode;
+ }
+ });
+ }
+
+ function parseHtmlStringToElement(htmlStr) {
+ const template = document.createElement('template')
+ template.insertAdjacentHTML('afterbegin', htmlStr)
+ return template.firstElementChild
+ }
+
+ function htmlEncode(htmlStr) {
+ return htmlStr.replace(/&/g, '&').replace(//g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''')
+ }
+
+ // get regex object from string
+ function getRegex(regex) {
+ try {
+ regex = regex.trim();
+ let parts = regex.split('/');
+ if (regex[0] !== '/' || parts.length < 3) {
+ regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string
+ return new RegExp(regex);
+ }
+
+ const option = parts[parts.length - 1];
+ const lastIndex = regex.lastIndexOf('/');
+ regex = regex.substring(1, lastIndex);
+ return new RegExp(regex, option);
+ } catch (e) {
+ return null
+ }
+ }
+
+ // Load file
+ function readFile(filePath) {
+ let request = new XMLHttpRequest();
+ request.open("GET", `file=${filePath}`, false);
+ request.send(null);
+ return request.responseText;
+ }
+
+ const logger = (function () {
+ const loggerTimerMap = new Map()
+ const loggerConf = { badge: true, label: 'Logger', enable: false }
+ return new Proxy(console, {
+ get: (target, propKey) => {
+ if (propKey === 'init') {
+ return (label) => {
+ loggerConf.label = label
+ loggerConf.enable = true
+ }
+ }
+
+ if (!(propKey in target)) return undefined
+
+ return (...args) => {
+ if (!loggerConf.enable) return
+
+ let color = ['#39cfe1', '#006cab']
+
+ let label, start
+ switch (propKey) {
+ case 'error':
+ color = ['#f70000', '#a70000']
+ break;
+ case 'warn':
+ color = ['#f7b500', '#b58400']
+ break;
+ case 'time':
+ label = args[0]
+ if (loggerTimerMap.has(label)) {
+ logger.warn(`Timer '${label}' already exisits`)
+ } else {
+ loggerTimerMap.set(label, performance.now())
+ }
+ return
+ case 'timeEnd':
+ label = args[0], start = loggerTimerMap.get(label)
+ if (start === undefined) {
+ logger.warn(`Timer '${label}' does not exist`)
+ } else {
+ loggerTimerMap.delete(label)
+ logger.log(`${label}: ${performance.now() - start} ms`)
+ }
+ return
+ case 'groupEnd':
+ loggerConf.badge = true
+ break
+ }
+
+ const badge = loggerConf.badge ? [`%c${loggerConf.label}`, `color: #fff; background: linear-gradient(180deg, ${color[0]}, ${color[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`] : []
+
+ target[propKey](...badge, ...args)
+
+ if (propKey === 'group' || propKey === 'groupCollapsed') {
+ loggerConf.badge = false
+ }
+ }
+ }
+ })
+ }())
+
+ function init() {
+ // Add style to dom
+ let $styleEL = document.createElement('style');
+
+ if ($styleEL.styleSheet) {
+ $styleEL.styleSheet.cssText = customCSS;
+ } else {
+ $styleEL.appendChild(document.createTextNode(customCSS));
+ }
+ gradioApp().appendChild($styleEL);
+
+ let loaded = false
+ let _count = 0
+
+ const observer = new MutationObserver(mutations => {
+ if (window.localization && Object.keys(window.localization).length) return; // disabled if original translation enabled
+ if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
+
+ let _nodesCount = 0, _now = performance.now()
+
+ for (const mutation of mutations) {
+ if (mutation.type === 'characterData') {
+ if (mutation.target?.parentElement?.parentElement?.tagName === 'LABEL') {
+ translateEl(mutation.target)
+ }
+ } else if (mutation.type === 'attributes') {
+ _nodesCount++
+ translateEl(mutation.target)
+ } else {
+ mutation.addedNodes.forEach(node => {
+ if (node.className === 'bilingual__trans_wrapper') return
+
+ _nodesCount++
+ if (node.nodeType === 1 && /(output|gradio)-(html|markdown)/.test(node.className)) {
+ translateEl(node, { rich: true })
+ } else if (node.nodeType === 3) {
+ doTranslate(node, node.textContent, 'text')
+ } else {
+ translateEl(node, { deep: true })
+ }
+ })
+ }
+ }
+
+ if (_nodesCount > 0) {
+ logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations)
+ }
+
+ if (loaded) return;
+ if (i18n) return;
+
+ loaded = true
+ setup()
+ })
+
+ observer.observe(gradioApp(), {
+ characterData: true,
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['title', 'placeholder']
+ })
+ }
+
+ // Init after page loaded
+ document.addEventListener('DOMContentLoaded', init)
+})();
diff --git a/src/config/opts.ts b/src/config/opts.ts
new file mode 100644
index 0000000..0cae4ec
--- /dev/null
+++ b/src/config/opts.ts
@@ -0,0 +1,9 @@
+import { Opts } from "../types/opts";
+
+export const opts: Opts = {
+ bilingual_localization_enabled: true, // 初期値を設定
+ bilingual_localization_logger: false, // 初期値を設定
+ bilingual_localization_file: "None", // 初期値を設定
+ bilingual_localization_dirs: "{}", // 初期値を設定
+ bilingual_localization_order: "Translation First", // 初期値を設定
+};
diff --git a/src/init.ts b/src/init.ts
new file mode 100644
index 0000000..5e68566
--- /dev/null
+++ b/src/init.ts
@@ -0,0 +1,169 @@
+import { opts } from "./config/opts";
+import { createLogger } from "./lib/create-logger";
+import { doTranslate } from "./lib/do-translate";
+import { gradioApp } from "./lib/gradio-app";
+import { translateEl } from "./lib/translate-el";
+import { getI18n, setup } from "./setup";
+
+const customCSS =
+ `
+ .bilingual__trans_wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ .bilingual__trans_wrapper em {
+ font-style: normal;
+ }
+
+ #txtimg_hr_finalres .bilingual__trans_wrapper em,
+ #tab_ti .output-html .bilingual__trans_wrapper em,
+ #tab_ti .gradio-html .bilingual__trans_wrapper em,
+ #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
+ #available_extensions .extension-tag .bilingual__trans_wrapper em,
+ #available_extensions .date_added .bilingual__trans_wrapper em,
+ #available_extensions+p>.bilingual__trans_wrapper em,
+ .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
+ display: none;
+ }
+
+ #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
+ label>span>.bilingual__trans_wrapper,
+ fieldset>span>.bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper,
+ .w-full>span>.bilingual__trans_wrapper,
+ .context-menu-items .bilingual__trans_wrapper,
+ .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
+ .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
+ .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
+ .output-markdown .bilingual__trans_wrapper,
+ .gradio-markdown .bilingual__trans_wrapper,
+ .gradio-image>div.float .bilingual__trans_wrapper,
+ .gradio-file>div.float .bilingual__trans_wrapper,
+ .gradio-code>div.float .bilingual__trans_wrapper,
+ .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
+ #dynamic-prompting .bilingual__trans_wrapper
+ {
+ font-size: 12px;
+ align-items: flex-start;
+ }
+
+ #extensions label .bilingual__trans_wrapper,
+ #available_extensions td .bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper {
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ .label-wrap>span:first-of-type {
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ #txt2img_script_container > div {
+ margin-top: var(--layout-gap, 12px);
+ }
+
+ textarea::placeholder {
+ line-height: 1;
+ padding: 4px 0;
+ }
+
+ label>span {
+ line-height: 1;
+ }
+
+ div[data-testid="image"] .start-prompt {
+ background-color: rgba(255, 255, 255, .6);
+ color: #222;
+ transition: opacity .2s ease-in-out;
+ }
+ div[data-testid="image"]:hover .start-prompt {
+ opacity: 0;
+ }
+
+ .label-wrap > span.icon {
+ width: 1em;
+ height: 1em;
+ transform-origin: center center;
+ }
+
+ .gradio-dropdown ul.options li.item {
+ padding: 0.3em 0.4em !important;
+ }
+
+ /* Posex extension */
+ .posex_bg {
+ white-space: nowrap;
+ }
+ `
+
+export function init() {
+ // Add style to dom
+ const styleEl = document.createElement('style');
+
+ if (styleEl.textContent) {
+ styleEl.textContent = customCSS;
+ } else {
+ styleEl.appendChild(document.createTextNode(customCSS));
+ }
+ gradioApp().appendChild(styleEl);
+
+ let loaded = false
+ let _count = 0
+
+ const observer = new MutationObserver(mutations => {
+ // @ts-ignore
+ if (window.localization && Object.keys(window.localization).length) return; // disabled if original translation enabled
+ if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
+
+ let _nodesCount = 0, _now = performance.now()
+
+ for (const mutation of mutations) {
+ if (mutation.type === 'characterData') {
+ if (mutation.target?.parentElement?.parentElement?.tagName === 'LABEL') {
+ translateEl(mutation.target)
+ }
+ } else if (mutation.type === 'attributes') {
+ _nodesCount++
+ translateEl(mutation.target)
+ } else {
+ mutation.addedNodes.forEach(node => {
+ if (node instanceof Element && node.className === 'bilingual__trans_wrapper') return; // NodeがElement型であることを確認
+
+ _nodesCount++;
+ if (node.nodeType === 1 && node instanceof Element && /(output|gradio)-(html|markdown)/.test(node.className)) { // nodeがElement型であることを確認
+ translateEl(node, { rich: true });
+ } else if (node.nodeType === 3) {
+ doTranslate(node, node.textContent, 'text');
+ } else {
+ translateEl(node, { deep: true });
+ }
+ });
+ }
+ }
+
+ if (_nodesCount > 0) {
+ const logger = createLogger()
+ logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations)
+ }
+
+ if (loaded) return;
+ const i18n = getI18n()
+ if (i18n) return;
+
+ loaded = true
+ setup()
+ })
+
+ observer.observe(gradioApp(), {
+ characterData: true,
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['title', 'placeholder']
+ })
+ }
\ No newline at end of file
diff --git a/src/lib/check-regax.ts b/src/lib/check-regax.ts
new file mode 100644
index 0000000..d71626b
--- /dev/null
+++ b/src/lib/check-regax.ts
@@ -0,0 +1,18 @@
+import { getI18nRegex } from "../setup";
+import { createLogger } from "./create-logger";
+
+export function checkRegex(source: string) {
+ const i18nRegex = getI18nRegex();
+ for (const [regex, value] of i18nRegex.entries()) {
+ if (regex instanceof RegExp) {
+ if (regex.test(source)) {
+ const logger = createLogger();
+ logger.log('regex', regex, source, value);
+ return source.replace(regex, value);
+ }
+ } else {
+ console.warn('Expected regex to be an instance of RegExp, but it was a string.');
+ }
+ }
+ return source;
+}
diff --git a/src/lib/create-logger.ts b/src/lib/create-logger.ts
new file mode 100644
index 0000000..442e255
--- /dev/null
+++ b/src/lib/create-logger.ts
@@ -0,0 +1,68 @@
+interface CustomConsole extends Console {
+ init: (label: string) => void;
+}
+
+export function createLogger(): CustomConsole {
+ const loggerTimerMap = new Map();
+ const loggerConf = { badge: true, label: 'Logger', enable: false };
+
+ return new Proxy(console, {
+ get: (target, propKey) => {
+ if (propKey === 'init') {
+ return (label: string) => {
+ loggerConf.label = label;
+ loggerConf.enable = true;
+ };
+ }
+
+ if (!(propKey in target)) return undefined;
+
+ return (...args: any[]) => {
+ if (!loggerConf.enable) return;
+
+ let color = ['#39cfe1', '#006cab'];
+
+ let label: string, start: number | undefined;
+ switch (propKey) {
+ case 'error':
+ color = ['#f70000', '#a70000'];
+ break;
+ case 'warn':
+ color = ['#f7b500', '#b58400'];
+ break;
+ case 'time':
+ label = args[0];
+ if (loggerTimerMap.has(label)) {
+ console.warn(`Timer '${label}' already exists`);
+ } else {
+ loggerTimerMap.set(label, performance.now());
+ }
+ return;
+ case 'timeEnd':
+ label = args[0];
+ start = loggerTimerMap.get(label);
+ if (start === undefined) {
+ console.warn(`Timer '${label}' does not exist`);
+ } else {
+ loggerTimerMap.delete(label);
+ console.log(`${label}: ${performance.now() - start} ms`);
+ }
+ return;
+ case 'groupEnd':
+ loggerConf.badge = true;
+ break;
+ }
+
+ const badge = loggerConf.badge
+ ? [`%c${loggerConf.label}`, `color: #fff; background: linear-gradient(180deg, ${color[0]}, ${color[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`]
+ : [];
+
+ (target as any)[propKey](...badge, ...args);
+
+ if (propKey === 'group' || propKey === 'groupCollapsed') {
+ loggerConf.badge = false;
+ }
+ };
+ }
+ }) as CustomConsole;
+}
\ No newline at end of file
diff --git a/src/lib/delegate-event.ts b/src/lib/delegate-event.ts
new file mode 100644
index 0000000..fc2dcbc
--- /dev/null
+++ b/src/lib/delegate-event.ts
@@ -0,0 +1,11 @@
+export function delegateEvent(parent, eventType, selector, handler) {
+ parent.addEventListener(eventType, function (event) {
+ var target = event.target;
+ while (target !== parent) {
+ if (target.matches(selector)) {
+ handler.call(target, event);
+ }
+ target = target.parentNode;
+ }
+ });
+ }
\ No newline at end of file
diff --git a/src/lib/do-translate.ts b/src/lib/do-translate.ts
new file mode 100644
index 0000000..586ac64
--- /dev/null
+++ b/src/lib/do-translate.ts
@@ -0,0 +1,92 @@
+import { getI18n, getI18nScope, getScopedSource, getConfig } from "../setup"
+import { checkRegex } from "./check-regax"
+import { htmlEncode } from "./html-encode"
+import { parseHtmlStringToElement } from "./parse-html-string-to-element"
+
+const re_num = /^[\.\d]+$/
+const re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
+
+export function doTranslate(el, source, type) {
+ if (!getI18n) return // translation not ready.
+ source = source.trim()
+ if (!source) return
+ if (re_num.test(source)) return
+ if (re_emoji.test(source)) return
+
+ let translation = getI18n[source] || checkRegex(source),
+ scopes = getScopedSource[source]
+
+ if (scopes) {
+ console.log('scope', el, source, scopes);
+ for (let scope of scopes) {
+ if (el.parentElement.closest(scope)) {
+ translation = getI18nScope[scope][source]
+ break
+ }
+ }
+ }
+
+ if (!translation || source === translation) {
+ if (el.textContent === '__biligual__will_be_replaced__') el.textContent = source // restore original text if translation not exist
+ if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() // remove exist translation if translation not exist
+ return
+ }
+
+ const config = getConfig()
+
+ if (config?.order === "Original First") {
+ [source, translation] = [translation, source]
+ }
+
+ switch (type) {
+ case 'text':
+ el.textContent = translation;
+ break;
+
+ case 'element':
+ const htmlStr = `${htmlEncode(translation)}${htmlEncode(source)}
`;
+ const htmlEl = parseHtmlStringToElement(htmlStr);
+
+ if (el.hasChildNodes()) {
+ const textNode = Array.from(el.childNodes).find(node =>
+ (node as Text).nodeType === Node.TEXT_NODE && // Ensure it's a text node
+ (node as Text).textContent?.trim() === source ||
+ (node as Text).textContent?.trim() === '__bilingual__will_be_replaced__'
+ ) as Text | undefined;
+
+ if (textNode) {
+ textNode.textContent = '';
+ if (textNode.nextSibling?.nodeType === Node.ELEMENT_NODE && (textNode.nextSibling as HTMLElement).className === 'bilingual__trans_wrapper') {
+ textNode.nextSibling.remove();
+ }
+ if (textNode.parentNode && htmlEl) { // Ensure htmlEl is not null
+ textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling);
+ }
+ }
+ } else {
+ el.textContent = '';
+ if (el.nextSibling?.nodeType === Node.ELEMENT_NODE && (el.nextSibling as HTMLElement).className === 'bilingual__trans_wrapper') {
+ el.nextSibling.remove();
+ }
+ if (el.parentNode && htmlEl) { // Ensure htmlEl is not null
+ el.parentNode.insertBefore(htmlEl, el.nextSibling);
+ }
+ }
+ break;
+
+ case 'option':
+ el.textContent = `${translation} (${source})`;
+ break;
+
+ case 'title':
+ el.title = `${translation}\n${source}`;
+ break;
+
+ case 'placeholder':
+ el.placeholder = `${translation}\n\n${source}`;
+ break;
+
+ default:
+ return translation;
+ }
+ }
\ No newline at end of file
diff --git a/src/lib/get-regax.ts b/src/lib/get-regax.ts
new file mode 100644
index 0000000..10bd839
--- /dev/null
+++ b/src/lib/get-regax.ts
@@ -0,0 +1,18 @@
+// get regex object from string
+export function getRegex(regex) {
+ try {
+ regex = regex.trim();
+ let parts = regex.split('/');
+ if (regex[0] !== '/' || parts.length < 3) {
+ regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string
+ return new RegExp(regex);
+ }
+
+ const option = parts[parts.length - 1];
+ const lastIndex = regex.lastIndexOf('/');
+ regex = regex.substring(1, lastIndex);
+ return new RegExp(regex, option);
+ } catch (e) {
+ return null
+ }
+ }
\ No newline at end of file
diff --git a/src/lib/gradio-app.ts b/src/lib/gradio-app.ts
new file mode 100644
index 0000000..8999c5a
--- /dev/null
+++ b/src/lib/gradio-app.ts
@@ -0,0 +1,22 @@
+export function gradioApp(): Document | ShadowRoot {
+ const elems = document.getElementsByTagName('gradio-app');
+ // @ts-ignore
+ const elem: Document | HTMLElement = elems.length === 0 ? document : elems[0];
+
+ if (elem !== document) {
+ (elem as any).getElementById = function (id: string): HTMLElement | null {
+ return document.getElementById(id);
+ };
+ }
+
+ return (elem as any).shadowRoot ? (elem as any).shadowRoot : elem;
+}
+
+export function querySelector(...args: Parameters): ReturnType | null {
+ return gradioApp()?.querySelector(...args) ?? null;
+}
+
+export function querySelectorAll(...args: Parameters): NodeListOf {
+ const nodeList = gradioApp()?.querySelectorAll(...args);
+ return nodeList || new NodeList();
+}
diff --git a/src/lib/handle-dropdown.ts b/src/lib/handle-dropdown.ts
new file mode 100644
index 0000000..6030507
--- /dev/null
+++ b/src/lib/handle-dropdown.ts
@@ -0,0 +1,26 @@
+import { delegateEvent } from "./delegate-event";
+import { doTranslate } from "./do-translate";
+import { gradioApp } from "./gradio-app";
+
+export function handleDropdown() {
+ // process gradio dropdown menu
+ delegateEvent(gradioApp(), 'mousedown', 'ul.options .item', function (event) {
+ const { target } = event
+
+ if (!target.classList.contains('item')) {
+ // simulate click menu item
+ target.closest('.item').dispatchEvent(new Event('mousedown', { bubbles: true }))
+ return
+ }
+
+ const source = target.dataset.value
+ const $labelEl = target?.closest('.wrap')?.querySelector('.wrap-inner .single-select') // the label element
+
+ if (source && $labelEl) {
+ // @ts-ignore
+ $labelEl.title = titles?.[source] || '' // set title from hints.js
+ $labelEl.textContent = "__biligual__will_be_replaced__" // marked as will be replaced
+ doTranslate($labelEl, source, 'element') // translate the label element
+ }
+ });
+ }
\ No newline at end of file
diff --git a/src/lib/html-encode.ts b/src/lib/html-encode.ts
new file mode 100644
index 0000000..98c4ed4
--- /dev/null
+++ b/src/lib/html-encode.ts
@@ -0,0 +1,4 @@
+export function htmlEncode(htmlStr) {
+ return htmlStr.replace(/&/g, '&').replace(//g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''')
+ }
\ No newline at end of file
diff --git a/src/lib/parse-html-string-to-element.ts b/src/lib/parse-html-string-to-element.ts
new file mode 100644
index 0000000..9b36106
--- /dev/null
+++ b/src/lib/parse-html-string-to-element.ts
@@ -0,0 +1,5 @@
+export function parseHtmlStringToElement(htmlStr) {
+ const template = document.createElement('template')
+ template.insertAdjacentHTML('afterbegin', htmlStr)
+ return template.firstElementChild
+}
\ No newline at end of file
diff --git a/src/lib/read-files.ts b/src/lib/read-files.ts
new file mode 100644
index 0000000..3cd9316
--- /dev/null
+++ b/src/lib/read-files.ts
@@ -0,0 +1,7 @@
+// Load file
+export function readFile(filePath) {
+ let request = new XMLHttpRequest();
+ request.open("GET", `file=${filePath}`, false);
+ request.send(null);
+ return request.responseText;
+}
\ No newline at end of file
diff --git a/src/lib/tranlate-page.ts b/src/lib/tranlate-page.ts
new file mode 100644
index 0000000..f2fcdd9
--- /dev/null
+++ b/src/lib/tranlate-page.ts
@@ -0,0 +1,44 @@
+import { getI18n } from "../setup"
+import { createLogger } from "./create-logger"
+import { querySelectorAll } from "./gradio-app"
+import { translateEl } from "./translate-el"
+
+export function translatePage() {
+ if (!getI18n()) return
+
+ const logger = createLogger()
+ logger.time('Full Page')
+
+
+ // Define arrays of selectors
+ const majorSelectors = [
+ "label span, fieldset span, button", // major label and button description text
+ "textarea[placeholder], select, option", // text box placeholder and select element
+ ".transition > div > span:not([class])", ".label-wrap > span", // collapse panel added by extension
+ ".gradio-image>div.float", // image upload description
+ ".gradio-file>div.float", // file upload description
+ ".gradio-code>div.float", // code editor description
+ "#modelmerger_interp_description .output-html", // model merger description
+ "#modelmerger_interp_description .gradio-html", // model merger description
+ "#lightboxModal span" // image preview lightbox
+ ];
+
+ const minorSelectors = [
+ 'div[data-testid="image"] > div > div', // description of image upload panel
+ '#extras_image_batch > div', // description of extras image batch file upload panel
+ ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", // output html exclude footer
+ '#dynamic-prompting' // dynamic-prompting extension
+ ];
+
+ // Process major selectors
+ majorSelectors.forEach(selector => {
+ querySelectorAll(selector).forEach(el => translateEl(el, { deep: true }))
+ });
+
+ // Process minor selectors
+ minorSelectors.forEach(selector => {
+ querySelectorAll(selector).forEach(el => translateEl(el, { rich: true }))
+ });
+
+ logger.timeEnd('Full Page')
+}
diff --git a/src/lib/translate-el.ts b/src/lib/translate-el.ts
new file mode 100644
index 0000000..91ff871
--- /dev/null
+++ b/src/lib/translate-el.ts
@@ -0,0 +1,48 @@
+import { getI18n } from "../setup"
+import { doTranslate } from "./do-translate"
+
+const ignore_selector = [
+ '.bilingual__trans_wrapper', // self
+ '.resultsFlexContainer', // tag-autocomplete
+ '#setting_sd_model_checkpoint select', // model checkpoint
+ '#setting_sd_vae select', // vae model
+ '#txt2img_styles, #img2txt_styles', // styles select
+ '.extra-network-cards .card .actions .name', // extra network cards name
+ 'script, style, svg, g, path', // script / style / svg elements
+]
+
+export function translateEl(el, { deep = false, rich = false } = {}) {
+ if (!getI18n) return // translation not ready.
+ if (el.matches?.(ignore_selector)) return // ignore some elements.
+
+ if (el.title) {
+ doTranslate(el, el.title, 'title')
+ }
+
+ if (el.placeholder) {
+ doTranslate(el, el.placeholder, 'placeholder')
+ }
+
+ if (el.tagName === 'OPTION') {
+ doTranslate(el, el.textContent, 'option')
+ }
+
+ if (deep || rich) {
+ Array.from(el.childNodes).forEach(node => {
+ if ((node as Text).nodeName === '#text') {
+ if (rich) {
+ doTranslate(node, (node as Text).textContent, 'text')
+ return
+ }
+
+ if (deep) {
+ doTranslate(node, (node as Text).textContent, 'element')
+ }
+ } else if ((node as Text).childNodes.length > 0) {
+ translateEl(node, { deep, rich })
+ }
+ })
+ } else {
+ doTranslate(el, el.textContent, 'element')
+ }
+ }
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..cccbc6d
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,31 @@
+import { init } from './init';
+interface I18n {
+ [key: string]: string;
+}
+
+interface I18nScope {
+ [scope: string]: I18n;
+}
+
+interface ScopedSource {
+ [source: string]: string[];
+}
+
+interface Config {
+ enabled: boolean;
+ file: string;
+ dirs: string[];
+ order: string;
+ enableLogger: boolean;
+}
+
+let i18n: I18n | null = null;
+let i18nRegex: Map = new Map();
+let i18nScope: I18nScope = {};
+let scopedSource: ScopedSource = {};
+let config: Config | null = null;
+
+// DOMContentLoaded イベント発生後に初期化処理を実行
+document.addEventListener('DOMContentLoaded', () => {
+ init()
+});
diff --git a/src/setup.ts b/src/setup.ts
new file mode 100644
index 0000000..c5579fa
--- /dev/null
+++ b/src/setup.ts
@@ -0,0 +1,99 @@
+import { readFile } from "./lib/read-files";
+import { getRegex } from "./lib/get-regax";
+import { createLogger } from "./lib/create-logger";
+import { opts } from "./config/opts";
+import { handleDropdown } from "./lib/handle-dropdown";
+import { translatePage } from "./lib/tranlate-page";
+
+interface Config {
+ enabled: boolean;
+ file: string;
+ dirs: string;
+ order: string;
+ enableLogger: boolean;
+}
+
+let i18n = null;
+const i18nRegex = new Map();
+const i18nScope = {};
+const scopedSource = {};
+let config: Config | null = null;
+
+export function setup() {
+ config = {
+ enabled: opts.bilingual_localization_enabled,
+ file: opts.bilingual_localization_file,
+ dirs: opts.bilingual_localization_dirs,
+ order: opts.bilingual_localization_order,
+ enableLogger: opts.bilingual_localization_logger,
+ };
+
+ let { enabled, file, dirs, enableLogger } = config;
+
+ if (!enabled || file === "None" || dirs === "None") return;
+ const dirsParsed = JSON.parse(dirs);
+
+ const logger = createLogger();
+ if (enableLogger) {
+ logger.init('Bilingual');
+ }
+ logger.log('Bilingual Localization initialized.');
+
+ // Load localization file
+ const regex_scope = /^##(?.+)##(?.+)$/; // ##scope##.skey
+ i18n = JSON.parse(readFile(dirsParsed[file]), (key, value) => {
+ // parse regex translations
+ if (key.startsWith('@@')) {
+ const regex = getRegex(key.slice(2));
+ if (regex instanceof RegExp) {
+ i18nRegex.set(regex, value);
+ }
+ } else {
+ const match = key.match(regex_scope);
+ if (match && match.groups) {
+ // parse scope translations
+ let { scope, skey } = match.groups;
+
+ if (scope.startsWith('@')) {
+ scope = scope.slice(1);
+ } else {
+ scope = '#' + scope;
+ }
+
+ if (!scope.length) {
+ return value;
+ }
+
+ i18nScope[scope] ||= {};
+ i18nScope[scope][skey] = value;
+
+ scopedSource[skey] ||= [];
+ scopedSource[skey].push(scope);
+ } else {
+ return value;
+ }
+ }
+ });
+ translatePage()
+ handleDropdown()
+}
+
+export function getI18n() {
+ return i18n;
+}
+
+export function getI18nRegex() {
+ return i18nRegex;
+}
+
+export function getI18nScope() {
+ return i18nScope;
+}
+
+export function getScopedSource() {
+ return scopedSource;
+}
+
+export function getConfig() {
+ return config;
+}
diff --git a/src/types/opts.d.ts b/src/types/opts.d.ts
new file mode 100644
index 0000000..35a0fba
--- /dev/null
+++ b/src/types/opts.d.ts
@@ -0,0 +1,8 @@
+export interface Opts {
+ bilingual_localization_enabled: boolean;
+ bilingual_localization_logger: boolean;
+ bilingual_localization_file: string;
+ bilingual_localization_dirs: string;
+ bilingual_localization_order: string;
+ }
+
\ No newline at end of file
From a287af64782c41660bd2e54b090c6eb4ebe0998f Mon Sep 17 00:00:00 2001
From: Katsuyuki-Karasawa <4ranci0ne@gmail.com>
Date: Sun, 4 Aug 2024 01:41:33 +0900
Subject: [PATCH 2/5] format and fix linter error
---
.vscode/extensions.json | 3 +
README.md | 6 +-
biome.json | 23 ++
javascript/bilingual_localization.js | 94 -------
src/config/opts.ts | 18 +-
src/init.ts | 353 ++++++++++++------------
src/lib/check-regax.ts | 38 +--
src/lib/create-logger.ts | 141 +++++-----
src/lib/delegate-event.ts | 22 +-
src/lib/do-translate.ts | 201 ++++++++------
src/lib/get-regax.ts | 42 +--
src/lib/gradio-app.ts | 49 ++--
src/lib/handle-dropdown.ts | 56 ++--
src/lib/html-encode.ts | 12 +-
src/lib/parse-html-string-to-element.ts | 10 +-
src/lib/read-files.ts | 14 +-
src/lib/tranlate-page.ts | 92 +++---
src/lib/translate-el.ts | 97 +++----
src/main.ts | 62 ++---
src/setup.ts | 199 ++++++-------
src/types/opts.d.ts | 15 +-
21 files changed, 773 insertions(+), 774 deletions(-)
create mode 100644 .vscode/extensions.json
create mode 100644 biome.json
delete mode 100644 javascript/bilingual_localization.js
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..583bad1
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["biomejs.biome"]
+}
diff --git a/README.md b/README.md
index f58220e..8bad01b 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,13 @@

-## Build
+## Development
```bash
+# format & lint
+bunx @biomejs/biome check --config-path=./biome.json
+
+# build
bun build ./src/main.ts --outfile ./javascript/bilingual_localization.js --minify
```
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..202c4e3
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "formatter": {
+ "indentStyle": "space",
+ "lineEnding": "crlf",
+ "ignore": ["./javascript", "./scripts"]
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ },
+ "ignore": ["./javascript", "./scripts"]
+ }
+}
diff --git a/javascript/bilingual_localization.js b/javascript/bilingual_localization.js
deleted file mode 100644
index f0d90da..0000000
--- a/javascript/bilingual_localization.js
+++ /dev/null
@@ -1,94 +0,0 @@
-var m={bilingual_localization_enabled:!0,bilingual_localization_logger:!1,bilingual_localization_file:"None",bilingual_localization_dirs:"{}",bilingual_localization_order:"Translation First"};function w(){const i=new Map,n={badge:!0,label:"Logger",enable:!1};return new Proxy(console,{get:(r,t)=>{if(t==="init")return(f)=>{n.label=f,n.enable=!0};if(!(t in r))return;return(...f)=>{if(!n.enable)return;let a=["#39cfe1","#006cab"],o,c;switch(t){case"error":a=["#f70000","#a70000"];break;case"warn":a=["#f7b500","#b58400"];break;case"time":if(o=f[0],i.has(o))console.warn(`Timer '${o}' already exists`);else i.set(o,performance.now());return;case"timeEnd":if(o=f[0],c=i.get(o),c===void 0)console.warn(`Timer '${o}' does not exist`);else i.delete(o),console.log(`${o}: ${performance.now()-c} ms`);return;case"groupEnd":n.badge=!0;break}const e=n.badge?[`%c${n.label}`,`color: #fff; background: linear-gradient(180deg, ${a[0]}, ${a[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`]:[];if(r[t](...e,...f),t==="group"||t==="groupCollapsed")n.badge=!1}}})}function q(i){let n=new XMLHttpRequest;return n.open("GET",`file=${i}`,!1),n.send(null),n.responseText}function C(i){try{i=i.trim();let n=i.split("/");if(i[0]!=="/"||n.length<3)return i=i.replace(/[.*+\-?^${}()|[\]\\]/g,"\\$&"),new RegExp(i);const r=n[n.length-1],t=i.lastIndexOf("/");return i=i.substring(1,t),new RegExp(i,r)}catch(n){return null}}function D(i,n,r,t){i.addEventListener(n,function(f){var a=f.target;while(a!==i){if(a.matches(r))t.call(a,f);a=a.parentNode}})}function x(){const i=document.getElementsByTagName("gradio-app"),n=i.length===0?document:i[0];if(n!==document)n.getElementById=function(r){return document.getElementById(r)};return n.shadowRoot?n.shadowRoot:n}function _(...i){return x()?.querySelectorAll(...i)||new NodeList}function M(){D(x(),"mousedown","ul.options .item",function(i){const{target:n}=i;if(!n.classList.contains("item")){n.closest(".item").dispatchEvent(new Event("mousedown",{bubbles:!0}));return}const r=n.dataset.value,t=n?.closest(".wrap")?.querySelector(".wrap-inner .single-select");if(r&&t)t.title=titles?.[r]||"",t.textContent="__biligual__will_be_replaced__",p(t,r,"element")})}function u(i,{deep:n=!1,rich:r=!1}={}){if(!l)return;if(i.matches?.(E))return;if(i.title)p(i,i.title,"title");if(i.placeholder)p(i,i.placeholder,"placeholder");if(i.tagName==="OPTION")p(i,i.textContent,"option");if(n||r)Array.from(i.childNodes).forEach((t)=>{if(t.nodeName==="#text"){if(r){p(t,t.textContent,"text");return}if(n)p(t,t.textContent,"element")}else if(t.childNodes.length>0)u(t,{deep:n,rich:r})});else p(i,i.textContent,"element")}var E=[".bilingual__trans_wrapper",".resultsFlexContainer","#setting_sd_model_checkpoint select","#setting_sd_vae select","#txt2img_styles, #img2txt_styles",".extra-network-cards .card .actions .name","script, style, svg, g, path"];function y(){if(!l())return;const i=w();i.time("Full Page");const n=["label span, fieldset span, button","textarea[placeholder], select, option",".transition > div > span:not([class])",".label-wrap > span",".gradio-image>div.float",".gradio-file>div.float",".gradio-code>div.float","#modelmerger_interp_description .output-html","#modelmerger_interp_description .gradio-html","#lightboxModal span"],r=['div[data-testid="image"] > div > div',"#extras_image_batch > div",".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown","#dynamic-prompting"];n.forEach((t)=>{_(t).forEach((f)=>u(f,{deep:!0}))}),r.forEach((t)=>{_(t).forEach((f)=>u(f,{rich:!0}))}),i.timeEnd("Full Page")}function T(){z={enabled:m.bilingual_localization_enabled,file:m.bilingual_localization_file,dirs:m.bilingual_localization_dirs,order:m.bilingual_localization_order,enableLogger:m.bilingual_localization_logger};let{enabled:i,file:n,dirs:r,enableLogger:t}=z;if(!i||n==="None"||r==="None")return;const f=JSON.parse(r),a=w();if(t)a.init("Bilingual");a.log("Bilingual Localization initialized.");const o=/^##(?.+)##(?.+)$/;B=JSON.parse(q(f[n]),(c,e)=>{if(c.startsWith("@@")){const g=C(c.slice(2));if(g instanceof RegExp)O.set(g,e)}else{const g=c.match(o);if(g&&g.groups){let{scope:b,skey:s}=g.groups;if(b.startsWith("@"))b=b.slice(1);else b="#"+b;if(!b.length)return e;R[b]||={},R[b][s]=e,L[s]||=[],L[s].push(b)}else return e}}),y(),M()}function l(){return B}function H(){return O}function S(){return R}function h(){return L}function k(){return z}var B=null,O=new Map,R={},L={},z=null;function v(i){const n=H();for(let[r,t]of n.entries())if(r instanceof RegExp){if(r.test(i))return w().log("regex",r,i,t),i.replace(r,t)}else console.warn("Expected regex to be an instance of RegExp, but it was a string.");return i}function $(i){return i.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function F(i){const n=document.createElement("template");return n.insertAdjacentHTML("afterbegin",i),n.firstElementChild}function p(i,n,r){if(!l)return;if(n=n.trim(),!n)return;if(I.test(n))return;if(J.test(n))return;let t=l[n]||v(n),f=h[n];if(f){console.log("scope",i,n,f);for(let o of f)if(i.parentElement.closest(o)){t=S[o][n];break}}if(!t||n===t){if(i.textContent==="__biligual__will_be_replaced__")i.textContent=n;if(i.nextSibling?.className==="bilingual__trans_wrapper")i.nextSibling.remove();return}if(k()?.order==="Original First")[n,t]=[t,n];switch(r){case"text":i.textContent=t;break;case"element":const o=`${$(t)}${$(n)}
`,c=F(o);if(i.hasChildNodes()){const e=Array.from(i.childNodes).find((g)=>g.nodeType===Node.TEXT_NODE&&g.textContent?.trim()===n||g.textContent?.trim()==="__bilingual__will_be_replaced__");if(e){if(e.textContent="",e.nextSibling?.nodeType===Node.ELEMENT_NODE&&e.nextSibling.className==="bilingual__trans_wrapper")e.nextSibling.remove();if(e.parentNode&&c)e.parentNode.insertBefore(c,e.nextSibling)}}else{if(i.textContent="",i.nextSibling?.nodeType===Node.ELEMENT_NODE&&i.nextSibling.className==="bilingual__trans_wrapper")i.nextSibling.remove();if(i.parentNode&&c)i.parentNode.insertBefore(c,i.nextSibling)}break;case"option":i.textContent=`${t} (${n})`;break;case"title":i.title=`${t}\n${n}`;break;case"placeholder":i.placeholder=`${t}\n\n${n}`;break;default:return t}}var I=/^[\.\d]+$/,J=/[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;function A(){const i=document.createElement("style");if(i.textContent)i.textContent=P;else i.appendChild(document.createTextNode(P));x().appendChild(i);let n=!1,r=0;new MutationObserver((f)=>{if(window.localization&&Object.keys(window.localization).length)return;if(Object.keys(m).length===0)return;let a=0,o=performance.now();for(let e of f)if(e.type==="characterData"){if(e.target?.parentElement?.parentElement?.tagName==="LABEL")u(e.target)}else if(e.type==="attributes")a++,u(e.target);else e.addedNodes.forEach((g)=>{if(g instanceof Element&&g.className==="bilingual__trans_wrapper")return;if(a++,g.nodeType===1&&g instanceof Element&&/(output|gradio)-(html|markdown)/.test(g.className))u(g,{rich:!0});else if(g.nodeType===3)p(g,g.textContent,"text");else u(g,{deep:!0})});if(a>0)w().info(`UI Update #${r++}: ${performance.now()-o} ms, ${a} nodes`,f);if(n)return;if(l())return;n=!0,T()}).observe(x(),{characterData:!0,childList:!0,subtree:!0,attributes:!0,attributeFilter:["title","placeholder"]})}var P=`
- .bilingual__trans_wrapper {
- display: inline-flex;
- flex-direction: column;
- align-items: center;
- font-size: 13px;
- line-height: 1;
- }
-
- .bilingual__trans_wrapper em {
- font-style: normal;
- }
-
- #txtimg_hr_finalres .bilingual__trans_wrapper em,
- #tab_ti .output-html .bilingual__trans_wrapper em,
- #tab_ti .gradio-html .bilingual__trans_wrapper em,
- #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
- #available_extensions .extension-tag .bilingual__trans_wrapper em,
- #available_extensions .date_added .bilingual__trans_wrapper em,
- #available_extensions+p>.bilingual__trans_wrapper em,
- .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
- display: none;
- }
-
- #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
- label>span>.bilingual__trans_wrapper,
- fieldset>span>.bilingual__trans_wrapper,
- .label-wrap>span>.bilingual__trans_wrapper,
- .w-full>span>.bilingual__trans_wrapper,
- .context-menu-items .bilingual__trans_wrapper,
- .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
- .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
- .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
- .output-markdown .bilingual__trans_wrapper,
- .gradio-markdown .bilingual__trans_wrapper,
- .gradio-image>div.float .bilingual__trans_wrapper,
- .gradio-file>div.float .bilingual__trans_wrapper,
- .gradio-code>div.float .bilingual__trans_wrapper,
- .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
- #dynamic-prompting .bilingual__trans_wrapper
- {
- font-size: 12px;
- align-items: flex-start;
- }
-
- #extensions label .bilingual__trans_wrapper,
- #available_extensions td .bilingual__trans_wrapper,
- .label-wrap>span>.bilingual__trans_wrapper {
- font-size: inherit;
- line-height: inherit;
- }
-
- .label-wrap>span:first-of-type {
- font-size: 13px;
- line-height: 1;
- }
-
- #txt2img_script_container > div {
- margin-top: var(--layout-gap, 12px);
- }
-
- textarea::placeholder {
- line-height: 1;
- padding: 4px 0;
- }
-
- label>span {
- line-height: 1;
- }
-
- div[data-testid="image"] .start-prompt {
- background-color: rgba(255, 255, 255, .6);
- color: #222;
- transition: opacity .2s ease-in-out;
- }
- div[data-testid="image"]:hover .start-prompt {
- opacity: 0;
- }
-
- .label-wrap > span.icon {
- width: 1em;
- height: 1em;
- transform-origin: center center;
- }
-
- .gradio-dropdown ul.options li.item {
- padding: 0.3em 0.4em !important;
- }
-
- /* Posex extension */
- .posex_bg {
- white-space: nowrap;
- }
- `;document.addEventListener("DOMContentLoaded",()=>{A()});
diff --git a/src/config/opts.ts b/src/config/opts.ts
index 0cae4ec..e14a959 100644
--- a/src/config/opts.ts
+++ b/src/config/opts.ts
@@ -1,9 +1,9 @@
-import { Opts } from "../types/opts";
-
-export const opts: Opts = {
- bilingual_localization_enabled: true, // 初期値を設定
- bilingual_localization_logger: false, // 初期値を設定
- bilingual_localization_file: "None", // 初期値を設定
- bilingual_localization_dirs: "{}", // 初期値を設定
- bilingual_localization_order: "Translation First", // 初期値を設定
-};
+import type { Opts } from "../types/opts";
+
+export const opts: Opts = {
+ bilingual_localization_enabled: true, // 初期値を設定
+ bilingual_localization_logger: false, // 初期値を設定
+ bilingual_localization_file: "None", // 初期値を設定
+ bilingual_localization_dirs: "{}", // 初期値を設定
+ bilingual_localization_order: "Translation First", // 初期値を設定
+};
diff --git a/src/init.ts b/src/init.ts
index 5e68566..77a4323 100644
--- a/src/init.ts
+++ b/src/init.ts
@@ -1,169 +1,184 @@
-import { opts } from "./config/opts";
-import { createLogger } from "./lib/create-logger";
-import { doTranslate } from "./lib/do-translate";
-import { gradioApp } from "./lib/gradio-app";
-import { translateEl } from "./lib/translate-el";
-import { getI18n, setup } from "./setup";
-
-const customCSS =
- `
- .bilingual__trans_wrapper {
- display: inline-flex;
- flex-direction: column;
- align-items: center;
- font-size: 13px;
- line-height: 1;
- }
-
- .bilingual__trans_wrapper em {
- font-style: normal;
- }
-
- #txtimg_hr_finalres .bilingual__trans_wrapper em,
- #tab_ti .output-html .bilingual__trans_wrapper em,
- #tab_ti .gradio-html .bilingual__trans_wrapper em,
- #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
- #available_extensions .extension-tag .bilingual__trans_wrapper em,
- #available_extensions .date_added .bilingual__trans_wrapper em,
- #available_extensions+p>.bilingual__trans_wrapper em,
- .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
- display: none;
- }
-
- #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
- label>span>.bilingual__trans_wrapper,
- fieldset>span>.bilingual__trans_wrapper,
- .label-wrap>span>.bilingual__trans_wrapper,
- .w-full>span>.bilingual__trans_wrapper,
- .context-menu-items .bilingual__trans_wrapper,
- .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
- .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
- .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
- .output-markdown .bilingual__trans_wrapper,
- .gradio-markdown .bilingual__trans_wrapper,
- .gradio-image>div.float .bilingual__trans_wrapper,
- .gradio-file>div.float .bilingual__trans_wrapper,
- .gradio-code>div.float .bilingual__trans_wrapper,
- .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
- #dynamic-prompting .bilingual__trans_wrapper
- {
- font-size: 12px;
- align-items: flex-start;
- }
-
- #extensions label .bilingual__trans_wrapper,
- #available_extensions td .bilingual__trans_wrapper,
- .label-wrap>span>.bilingual__trans_wrapper {
- font-size: inherit;
- line-height: inherit;
- }
-
- .label-wrap>span:first-of-type {
- font-size: 13px;
- line-height: 1;
- }
-
- #txt2img_script_container > div {
- margin-top: var(--layout-gap, 12px);
- }
-
- textarea::placeholder {
- line-height: 1;
- padding: 4px 0;
- }
-
- label>span {
- line-height: 1;
- }
-
- div[data-testid="image"] .start-prompt {
- background-color: rgba(255, 255, 255, .6);
- color: #222;
- transition: opacity .2s ease-in-out;
- }
- div[data-testid="image"]:hover .start-prompt {
- opacity: 0;
- }
-
- .label-wrap > span.icon {
- width: 1em;
- height: 1em;
- transform-origin: center center;
- }
-
- .gradio-dropdown ul.options li.item {
- padding: 0.3em 0.4em !important;
- }
-
- /* Posex extension */
- .posex_bg {
- white-space: nowrap;
- }
- `
-
-export function init() {
- // Add style to dom
- const styleEl = document.createElement('style');
-
- if (styleEl.textContent) {
- styleEl.textContent = customCSS;
- } else {
- styleEl.appendChild(document.createTextNode(customCSS));
- }
- gradioApp().appendChild(styleEl);
-
- let loaded = false
- let _count = 0
-
- const observer = new MutationObserver(mutations => {
- // @ts-ignore
- if (window.localization && Object.keys(window.localization).length) return; // disabled if original translation enabled
- if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
-
- let _nodesCount = 0, _now = performance.now()
-
- for (const mutation of mutations) {
- if (mutation.type === 'characterData') {
- if (mutation.target?.parentElement?.parentElement?.tagName === 'LABEL') {
- translateEl(mutation.target)
- }
- } else if (mutation.type === 'attributes') {
- _nodesCount++
- translateEl(mutation.target)
- } else {
- mutation.addedNodes.forEach(node => {
- if (node instanceof Element && node.className === 'bilingual__trans_wrapper') return; // NodeがElement型であることを確認
-
- _nodesCount++;
- if (node.nodeType === 1 && node instanceof Element && /(output|gradio)-(html|markdown)/.test(node.className)) { // nodeがElement型であることを確認
- translateEl(node, { rich: true });
- } else if (node.nodeType === 3) {
- doTranslate(node, node.textContent, 'text');
- } else {
- translateEl(node, { deep: true });
- }
- });
- }
- }
-
- if (_nodesCount > 0) {
- const logger = createLogger()
- logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations)
- }
-
- if (loaded) return;
- const i18n = getI18n()
- if (i18n) return;
-
- loaded = true
- setup()
- })
-
- observer.observe(gradioApp(), {
- characterData: true,
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['title', 'placeholder']
- })
- }
\ No newline at end of file
+import { opts } from "./config/opts";
+import { createLogger } from "./lib/create-logger";
+import { doTranslate } from "./lib/do-translate";
+import { gradioApp } from "./lib/gradio-app";
+import { translateEl } from "./lib/translate-el";
+import { getI18n, setup } from "./setup";
+
+const customCSS = `
+ .bilingual__trans_wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ .bilingual__trans_wrapper em {
+ font-style: normal;
+ }
+
+ #txtimg_hr_finalres .bilingual__trans_wrapper em,
+ #tab_ti .output-html .bilingual__trans_wrapper em,
+ #tab_ti .gradio-html .bilingual__trans_wrapper em,
+ #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
+ #available_extensions .extension-tag .bilingual__trans_wrapper em,
+ #available_extensions .date_added .bilingual__trans_wrapper em,
+ #available_extensions+p>.bilingual__trans_wrapper em,
+ .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
+ display: none;
+ }
+
+ #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
+ label>span>.bilingual__trans_wrapper,
+ fieldset>span>.bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper,
+ .w-full>span>.bilingual__trans_wrapper,
+ .context-menu-items .bilingual__trans_wrapper,
+ .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
+ .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
+ .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
+ .output-markdown .bilingual__trans_wrapper,
+ .gradio-markdown .bilingual__trans_wrapper,
+ .gradio-image>div.float .bilingual__trans_wrapper,
+ .gradio-file>div.float .bilingual__trans_wrapper,
+ .gradio-code>div.float .bilingual__trans_wrapper,
+ .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
+ #dynamic-prompting .bilingual__trans_wrapper
+ {
+ font-size: 12px;
+ align-items: flex-start;
+ }
+
+ #extensions label .bilingual__trans_wrapper,
+ #available_extensions td .bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper {
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ .label-wrap>span:first-of-type {
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ #txt2img_script_container > div {
+ margin-top: var(--layout-gap, 12px);
+ }
+
+ textarea::placeholder {
+ line-height: 1;
+ padding: 4px 0;
+ }
+
+ label>span {
+ line-height: 1;
+ }
+
+ div[data-testid="image"] .start-prompt {
+ background-color: rgba(255, 255, 255, .6);
+ color: #222;
+ transition: opacity .2s ease-in-out;
+ }
+ div[data-testid="image"]:hover .start-prompt {
+ opacity: 0;
+ }
+
+ .label-wrap > span.icon {
+ width: 1em;
+ height: 1em;
+ transform-origin: center center;
+ }
+
+ .gradio-dropdown ul.options li.item {
+ padding: 0.3em 0.4em !important;
+ }
+
+ /* Posex extension */
+ .posex_bg {
+ white-space: nowrap;
+ }
+ `;
+
+export function init() {
+ // Add style to dom
+ const styleEl = document.createElement("style");
+
+ if (styleEl.textContent) {
+ styleEl.textContent = customCSS;
+ } else {
+ styleEl.appendChild(document.createTextNode(customCSS));
+ }
+ gradioApp().appendChild(styleEl);
+
+ let loaded = false;
+ let _count = 0;
+
+ const observer = new MutationObserver((mutations) => {
+ // @ts-ignore
+ if (window.localization && Object.keys(window.localization).length) return; // disabled if original translation enabled
+ if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
+
+ let _nodesCount = 0;
+ const _now = performance.now();
+
+ for (const mutation of mutations) {
+ if (mutation.type === "characterData") {
+ if (
+ mutation.target?.parentElement?.parentElement?.tagName === "LABEL"
+ ) {
+ translateEl(mutation.target);
+ }
+ } else if (mutation.type === "attributes") {
+ _nodesCount++;
+ translateEl(mutation.target);
+ } else {
+ // biome-ignore lint/complexity/noForEach:
+ mutation.addedNodes.forEach((node) => {
+ if (
+ node instanceof Element &&
+ node.className === "bilingual__trans_wrapper"
+ )
+ return; // NodeがElement型であることを確認
+
+ _nodesCount++;
+ if (
+ node.nodeType === 1 &&
+ node instanceof Element &&
+ /(output|gradio)-(html|markdown)/.test(node.className)
+ ) {
+ // nodeがElement型であることを確認
+ translateEl(node, { rich: true });
+ } else if (node.nodeType === 3) {
+ doTranslate(node, node.textContent, "text");
+ } else {
+ translateEl(node, { deep: true });
+ }
+ });
+ }
+ }
+
+ if (_nodesCount > 0) {
+ const logger = createLogger();
+ logger.info(
+ `UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`,
+ mutations,
+ );
+ }
+
+ if (loaded) return;
+ const i18n = getI18n();
+ if (i18n) return;
+
+ loaded = true;
+ setup();
+ });
+
+ observer.observe(gradioApp(), {
+ characterData: true,
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["title", "placeholder"],
+ });
+}
diff --git a/src/lib/check-regax.ts b/src/lib/check-regax.ts
index d71626b..e58dd0e 100644
--- a/src/lib/check-regax.ts
+++ b/src/lib/check-regax.ts
@@ -1,18 +1,20 @@
-import { getI18nRegex } from "../setup";
-import { createLogger } from "./create-logger";
-
-export function checkRegex(source: string) {
- const i18nRegex = getI18nRegex();
- for (const [regex, value] of i18nRegex.entries()) {
- if (regex instanceof RegExp) {
- if (regex.test(source)) {
- const logger = createLogger();
- logger.log('regex', regex, source, value);
- return source.replace(regex, value);
- }
- } else {
- console.warn('Expected regex to be an instance of RegExp, but it was a string.');
- }
- }
- return source;
-}
+import { getI18nRegex } from "../setup";
+import { createLogger } from "./create-logger";
+
+export function checkRegex(source: string) {
+ const i18nRegex = getI18nRegex();
+ for (const [regex, value] of i18nRegex.entries()) {
+ if (regex instanceof RegExp) {
+ if (regex.test(source)) {
+ const logger = createLogger();
+ logger.log("regex", regex, source, value);
+ return source.replace(regex, value);
+ }
+ } else {
+ console.warn(
+ "Expected regex to be an instance of RegExp, but it was a string.",
+ );
+ }
+ }
+ return source;
+}
diff --git a/src/lib/create-logger.ts b/src/lib/create-logger.ts
index 442e255..74e9d7c 100644
--- a/src/lib/create-logger.ts
+++ b/src/lib/create-logger.ts
@@ -1,68 +1,73 @@
-interface CustomConsole extends Console {
- init: (label: string) => void;
-}
-
-export function createLogger(): CustomConsole {
- const loggerTimerMap = new Map();
- const loggerConf = { badge: true, label: 'Logger', enable: false };
-
- return new Proxy(console, {
- get: (target, propKey) => {
- if (propKey === 'init') {
- return (label: string) => {
- loggerConf.label = label;
- loggerConf.enable = true;
- };
- }
-
- if (!(propKey in target)) return undefined;
-
- return (...args: any[]) => {
- if (!loggerConf.enable) return;
-
- let color = ['#39cfe1', '#006cab'];
-
- let label: string, start: number | undefined;
- switch (propKey) {
- case 'error':
- color = ['#f70000', '#a70000'];
- break;
- case 'warn':
- color = ['#f7b500', '#b58400'];
- break;
- case 'time':
- label = args[0];
- if (loggerTimerMap.has(label)) {
- console.warn(`Timer '${label}' already exists`);
- } else {
- loggerTimerMap.set(label, performance.now());
- }
- return;
- case 'timeEnd':
- label = args[0];
- start = loggerTimerMap.get(label);
- if (start === undefined) {
- console.warn(`Timer '${label}' does not exist`);
- } else {
- loggerTimerMap.delete(label);
- console.log(`${label}: ${performance.now() - start} ms`);
- }
- return;
- case 'groupEnd':
- loggerConf.badge = true;
- break;
- }
-
- const badge = loggerConf.badge
- ? [`%c${loggerConf.label}`, `color: #fff; background: linear-gradient(180deg, ${color[0]}, ${color[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`]
- : [];
-
- (target as any)[propKey](...badge, ...args);
-
- if (propKey === 'group' || propKey === 'groupCollapsed') {
- loggerConf.badge = false;
- }
- };
- }
- }) as CustomConsole;
-}
\ No newline at end of file
+interface CustomConsole extends Console {
+ init: (label: string) => void;
+}
+
+export function createLogger(): CustomConsole {
+ const loggerTimerMap = new Map();
+ const loggerConf = { badge: true, label: "Logger", enable: false };
+
+ return new Proxy(console, {
+ get: (target, propKey) => {
+ if (propKey === "init") {
+ return (label: string) => {
+ loggerConf.label = label;
+ loggerConf.enable = true;
+ };
+ }
+
+ if (!(propKey in target)) return undefined;
+
+ // biome-ignore lint/suspicious/noExplicitAny:
+ return (...args: any[]) => {
+ if (!loggerConf.enable) return;
+
+ let color = ["#39cfe1", "#006cab"];
+
+ let label: string;
+ let start: number | undefined;
+ switch (propKey) {
+ case "error":
+ color = ["#f70000", "#a70000"];
+ break;
+ case "warn":
+ color = ["#f7b500", "#b58400"];
+ break;
+ case "time":
+ label = args[0];
+ if (loggerTimerMap.has(label)) {
+ console.warn(`Timer '${label}' already exists`);
+ } else {
+ loggerTimerMap.set(label, performance.now());
+ }
+ return;
+ case "timeEnd":
+ label = args[0];
+ start = loggerTimerMap.get(label);
+ if (start === undefined) {
+ console.warn(`Timer '${label}' does not exist`);
+ } else {
+ loggerTimerMap.delete(label);
+ console.log(`${label}: ${performance.now() - start} ms`);
+ }
+ return;
+ case "groupEnd":
+ loggerConf.badge = true;
+ break;
+ }
+
+ const badge = loggerConf.badge
+ ? [
+ `%c${loggerConf.label}`,
+ `color: #fff; background: linear-gradient(180deg, ${color[0]}, ${color[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`,
+ ]
+ : [];
+
+ target[propKey](...badge, ...args);
+
+ if (propKey === "group" || propKey === "groupCollapsed") {
+ loggerConf.badge = false;
+ }
+ };
+ },
+ }) as CustomConsole;
+}
diff --git a/src/lib/delegate-event.ts b/src/lib/delegate-event.ts
index fc2dcbc..803676c 100644
--- a/src/lib/delegate-event.ts
+++ b/src/lib/delegate-event.ts
@@ -1,11 +1,11 @@
-export function delegateEvent(parent, eventType, selector, handler) {
- parent.addEventListener(eventType, function (event) {
- var target = event.target;
- while (target !== parent) {
- if (target.matches(selector)) {
- handler.call(target, event);
- }
- target = target.parentNode;
- }
- });
- }
\ No newline at end of file
+export function delegateEvent(parent, eventType, selector, handler) {
+ parent.addEventListener(eventType, (event) => {
+ let target = event.target;
+ while (target !== parent) {
+ if (target.matches(selector)) {
+ handler.call(target, event);
+ }
+ target = target.parentNode;
+ }
+ });
+}
diff --git a/src/lib/do-translate.ts b/src/lib/do-translate.ts
index 586ac64..3082b9c 100644
--- a/src/lib/do-translate.ts
+++ b/src/lib/do-translate.ts
@@ -1,92 +1,109 @@
-import { getI18n, getI18nScope, getScopedSource, getConfig } from "../setup"
-import { checkRegex } from "./check-regax"
-import { htmlEncode } from "./html-encode"
-import { parseHtmlStringToElement } from "./parse-html-string-to-element"
-
-const re_num = /^[\.\d]+$/
-const re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
-
-export function doTranslate(el, source, type) {
- if (!getI18n) return // translation not ready.
- source = source.trim()
- if (!source) return
- if (re_num.test(source)) return
- if (re_emoji.test(source)) return
-
- let translation = getI18n[source] || checkRegex(source),
- scopes = getScopedSource[source]
-
- if (scopes) {
- console.log('scope', el, source, scopes);
- for (let scope of scopes) {
- if (el.parentElement.closest(scope)) {
- translation = getI18nScope[scope][source]
- break
- }
- }
- }
-
- if (!translation || source === translation) {
- if (el.textContent === '__biligual__will_be_replaced__') el.textContent = source // restore original text if translation not exist
- if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() // remove exist translation if translation not exist
- return
- }
-
- const config = getConfig()
-
- if (config?.order === "Original First") {
- [source, translation] = [translation, source]
- }
-
- switch (type) {
- case 'text':
- el.textContent = translation;
- break;
-
- case 'element':
- const htmlStr = `${htmlEncode(translation)}${htmlEncode(source)}
`;
- const htmlEl = parseHtmlStringToElement(htmlStr);
-
- if (el.hasChildNodes()) {
- const textNode = Array.from(el.childNodes).find(node =>
- (node as Text).nodeType === Node.TEXT_NODE && // Ensure it's a text node
- (node as Text).textContent?.trim() === source ||
- (node as Text).textContent?.trim() === '__bilingual__will_be_replaced__'
- ) as Text | undefined;
-
- if (textNode) {
- textNode.textContent = '';
- if (textNode.nextSibling?.nodeType === Node.ELEMENT_NODE && (textNode.nextSibling as HTMLElement).className === 'bilingual__trans_wrapper') {
- textNode.nextSibling.remove();
- }
- if (textNode.parentNode && htmlEl) { // Ensure htmlEl is not null
- textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling);
- }
- }
- } else {
- el.textContent = '';
- if (el.nextSibling?.nodeType === Node.ELEMENT_NODE && (el.nextSibling as HTMLElement).className === 'bilingual__trans_wrapper') {
- el.nextSibling.remove();
- }
- if (el.parentNode && htmlEl) { // Ensure htmlEl is not null
- el.parentNode.insertBefore(htmlEl, el.nextSibling);
- }
- }
- break;
-
- case 'option':
- el.textContent = `${translation} (${source})`;
- break;
-
- case 'title':
- el.title = `${translation}\n${source}`;
- break;
-
- case 'placeholder':
- el.placeholder = `${translation}\n\n${source}`;
- break;
-
- default:
- return translation;
- }
- }
\ No newline at end of file
+import { getConfig, getI18n, getI18nScope, getScopedSource } from "../setup";
+import { checkRegex } from "./check-regax";
+import { htmlEncode } from "./html-encode";
+import { parseHtmlStringToElement } from "./parse-html-string-to-element";
+
+const re_num = /^[\.\d]+$/;
+const re_emoji =
+ // biome-ignore lint/suspicious/noMisleadingCharacterClass:
+ /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;
+
+export function doTranslate(el, source, type) {
+ if (!getI18n) return; // translation not ready.
+ let trimmedSource = source.trim();
+ if (!trimmedSource) return;
+ if (re_num.test(trimmedSource)) return;
+ if (re_emoji.test(trimmedSource)) return;
+
+ let translation = getI18n[trimmedSource] || checkRegex(trimmedSource);
+ const scopes = getScopedSource[trimmedSource];
+
+ if (scopes) {
+ console.log("scope", el, trimmedSource, scopes);
+ for (const scope of scopes) {
+ if (el.parentElement.closest(scope)) {
+ translation = getI18nScope[scope][trimmedSource];
+ break;
+ }
+ }
+ }
+
+ if (!translation || trimmedSource === translation) {
+ if (el.textContent === "__biligual__will_be_replaced__")
+ el.textContent = trimmedSource; // restore original text if translation not exist
+ if (el.nextSibling?.className === "bilingual__trans_wrapper")
+ el.nextSibling.remove(); // remove exist translation if translation not exist
+ return;
+ }
+
+ const config = getConfig();
+
+ if (config?.order === "Original First") {
+ [trimmedSource, translation] = [translation, trimmedSource];
+ }
+
+ switch (type) {
+ case "text":
+ el.textContent = translation;
+ break;
+
+ case "element": {
+ const htmlStr = `${htmlEncode(translation)}${htmlEncode(trimmedSource)}
`;
+ const htmlEl = parseHtmlStringToElement(htmlStr);
+
+ if (el.hasChildNodes()) {
+ const textNode = Array.from(el.childNodes).find(
+ (node) =>
+ ((node as Text).nodeType === Node.TEXT_NODE && // Ensure it's a text node
+ (node as Text).textContent?.trim() === trimmedSource) ||
+ (node as Text).textContent?.trim() ===
+ "__bilingual__will_be_replaced__",
+ ) as Text | undefined;
+
+ if (textNode) {
+ textNode.textContent = "";
+ if (
+ textNode.nextSibling?.nodeType === Node.ELEMENT_NODE &&
+ (textNode.nextSibling as HTMLElement).className ===
+ "bilingual__trans_wrapper"
+ ) {
+ textNode.nextSibling.remove();
+ }
+ if (textNode.parentNode && htmlEl) {
+ // Ensure htmlEl is not null
+ textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling);
+ }
+ }
+ } else {
+ el.textContent = "";
+ if (
+ el.nextSibling?.nodeType === Node.ELEMENT_NODE &&
+ (el.nextSibling as HTMLElement).className ===
+ "bilingual__trans_wrapper"
+ ) {
+ el.nextSibling.remove();
+ }
+ if (el.parentNode && htmlEl) {
+ // Ensure htmlEl is not null
+ el.parentNode.insertBefore(htmlEl, el.nextSibling);
+ }
+ }
+ break;
+ }
+
+ case "option":
+ el.textContent = `${translation} (${trimmedSource})`;
+ break;
+
+ case "title":
+ el.title = `${translation}\n${trimmedSource}`;
+ break;
+
+ case "placeholder":
+ el.placeholder = `${translation}\n\n${trimmedSource}`;
+ break;
+
+ default:
+ return translation;
+ }
+}
diff --git a/src/lib/get-regax.ts b/src/lib/get-regax.ts
index 10bd839..673b0de 100644
--- a/src/lib/get-regax.ts
+++ b/src/lib/get-regax.ts
@@ -1,18 +1,24 @@
-// get regex object from string
-export function getRegex(regex) {
- try {
- regex = regex.trim();
- let parts = regex.split('/');
- if (regex[0] !== '/' || parts.length < 3) {
- regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string
- return new RegExp(regex);
- }
-
- const option = parts[parts.length - 1];
- const lastIndex = regex.lastIndexOf('/');
- regex = regex.substring(1, lastIndex);
- return new RegExp(regex, option);
- } catch (e) {
- return null
- }
- }
\ No newline at end of file
+// get regex object from string
+export function getRegex(regexString: string): RegExp | null {
+ try {
+ const trimmedRegexString = regexString.trim();
+
+ if (
+ !trimmedRegexString.startsWith("/") ||
+ trimmedRegexString.split("/").length < 3
+ ) {
+ const escapedRegexString = trimmedRegexString.replace(
+ /[.*+\-?^${}()|[\]\\]/g,
+ "\\$&",
+ );
+ return new RegExp(escapedRegexString);
+ }
+
+ const lastSlashIndex = trimmedRegexString.lastIndexOf("/");
+ const regexPattern = trimmedRegexString.slice(1, lastSlashIndex);
+ const regexFlags = trimmedRegexString.slice(lastSlashIndex + 1);
+ return new RegExp(regexPattern, regexFlags);
+ } catch (e) {
+ return null;
+ }
+}
diff --git a/src/lib/gradio-app.ts b/src/lib/gradio-app.ts
index 8999c5a..c5c8fac 100644
--- a/src/lib/gradio-app.ts
+++ b/src/lib/gradio-app.ts
@@ -1,22 +1,27 @@
-export function gradioApp(): Document | ShadowRoot {
- const elems = document.getElementsByTagName('gradio-app');
- // @ts-ignore
- const elem: Document | HTMLElement = elems.length === 0 ? document : elems[0];
-
- if (elem !== document) {
- (elem as any).getElementById = function (id: string): HTMLElement | null {
- return document.getElementById(id);
- };
- }
-
- return (elem as any).shadowRoot ? (elem as any).shadowRoot : elem;
-}
-
-export function querySelector(...args: Parameters): ReturnType | null {
- return gradioApp()?.querySelector(...args) ?? null;
-}
-
-export function querySelectorAll(...args: Parameters): NodeListOf {
- const nodeList = gradioApp()?.querySelectorAll(...args);
- return nodeList || new NodeList();
-}
+export function gradioApp(): Document | ShadowRoot {
+ const elems = document.getElementsByTagName("gradio-app");
+ // @ts-ignore
+ const elem: Document | HTMLElement = elems.length === 0 ? document : elems[0];
+
+ if (elem !== document) {
+ // biome-ignore lint/suspicious/noExplicitAny:
+ (elem as any).getElementById = (id: string): HTMLElement | null =>
+ document.getElementById(id);
+ }
+
+ // biome-ignore lint/suspicious/noExplicitAny:
+ return (elem as any).shadowRoot ? (elem as any).shadowRoot : elem;
+}
+
+export function querySelector(
+ ...args: Parameters
+): ReturnType | null {
+ return gradioApp()?.querySelector(...args) ?? null;
+}
+
+export function querySelectorAll(
+ ...args: Parameters
+): NodeListOf {
+ const nodeList = gradioApp()?.querySelectorAll(...args);
+ return nodeList || new NodeList();
+}
diff --git a/src/lib/handle-dropdown.ts b/src/lib/handle-dropdown.ts
index 6030507..629b9a3 100644
--- a/src/lib/handle-dropdown.ts
+++ b/src/lib/handle-dropdown.ts
@@ -1,26 +1,30 @@
-import { delegateEvent } from "./delegate-event";
-import { doTranslate } from "./do-translate";
-import { gradioApp } from "./gradio-app";
-
-export function handleDropdown() {
- // process gradio dropdown menu
- delegateEvent(gradioApp(), 'mousedown', 'ul.options .item', function (event) {
- const { target } = event
-
- if (!target.classList.contains('item')) {
- // simulate click menu item
- target.closest('.item').dispatchEvent(new Event('mousedown', { bubbles: true }))
- return
- }
-
- const source = target.dataset.value
- const $labelEl = target?.closest('.wrap')?.querySelector('.wrap-inner .single-select') // the label element
-
- if (source && $labelEl) {
- // @ts-ignore
- $labelEl.title = titles?.[source] || '' // set title from hints.js
- $labelEl.textContent = "__biligual__will_be_replaced__" // marked as will be replaced
- doTranslate($labelEl, source, 'element') // translate the label element
- }
- });
- }
\ No newline at end of file
+import { delegateEvent } from "./delegate-event";
+import { doTranslate } from "./do-translate";
+import { gradioApp } from "./gradio-app";
+
+export function handleDropdown() {
+ // process gradio dropdown menu
+ delegateEvent(gradioApp(), "mousedown", "ul.options .item", (event) => {
+ const { target } = event;
+
+ if (!target.classList.contains("item")) {
+ // simulate click menu item
+ target
+ .closest(".item")
+ .dispatchEvent(new Event("mousedown", { bubbles: true }));
+ return;
+ }
+
+ const source = target.dataset.value;
+ const $labelEl = target
+ ?.closest(".wrap")
+ ?.querySelector(".wrap-inner .single-select"); // the label element
+
+ if (source && $labelEl) {
+ // @ts-ignore
+ $labelEl.title = titles?.[source] || ""; // set title from hints.js
+ $labelEl.textContent = "__biligual__will_be_replaced__"; // marked as will be replaced
+ doTranslate($labelEl, source, "element"); // translate the label element
+ }
+ });
+}
diff --git a/src/lib/html-encode.ts b/src/lib/html-encode.ts
index 98c4ed4..9aa02ae 100644
--- a/src/lib/html-encode.ts
+++ b/src/lib/html-encode.ts
@@ -1,4 +1,8 @@
-export function htmlEncode(htmlStr) {
- return htmlStr.replace(/&/g, '&').replace(//g, '>')
- .replace(/"/g, '"').replace(/'/g, ''')
- }
\ No newline at end of file
+export function htmlEncode(htmlStr) {
+ return htmlStr
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
diff --git a/src/lib/parse-html-string-to-element.ts b/src/lib/parse-html-string-to-element.ts
index 9b36106..73412ef 100644
--- a/src/lib/parse-html-string-to-element.ts
+++ b/src/lib/parse-html-string-to-element.ts
@@ -1,5 +1,5 @@
-export function parseHtmlStringToElement(htmlStr) {
- const template = document.createElement('template')
- template.insertAdjacentHTML('afterbegin', htmlStr)
- return template.firstElementChild
-}
\ No newline at end of file
+export function parseHtmlStringToElement(htmlStr) {
+ const template = document.createElement("template");
+ template.insertAdjacentHTML("afterbegin", htmlStr);
+ return template.firstElementChild;
+}
diff --git a/src/lib/read-files.ts b/src/lib/read-files.ts
index 3cd9316..a2b5787 100644
--- a/src/lib/read-files.ts
+++ b/src/lib/read-files.ts
@@ -1,7 +1,7 @@
-// Load file
-export function readFile(filePath) {
- let request = new XMLHttpRequest();
- request.open("GET", `file=${filePath}`, false);
- request.send(null);
- return request.responseText;
-}
\ No newline at end of file
+// Load file
+export function readFile(filePath) {
+ const request = new XMLHttpRequest();
+ request.open("GET", `file=${filePath}`, false);
+ request.send(null);
+ return request.responseText;
+}
diff --git a/src/lib/tranlate-page.ts b/src/lib/tranlate-page.ts
index f2fcdd9..b8e10cc 100644
--- a/src/lib/tranlate-page.ts
+++ b/src/lib/tranlate-page.ts
@@ -1,44 +1,48 @@
-import { getI18n } from "../setup"
-import { createLogger } from "./create-logger"
-import { querySelectorAll } from "./gradio-app"
-import { translateEl } from "./translate-el"
-
-export function translatePage() {
- if (!getI18n()) return
-
- const logger = createLogger()
- logger.time('Full Page')
-
-
- // Define arrays of selectors
- const majorSelectors = [
- "label span, fieldset span, button", // major label and button description text
- "textarea[placeholder], select, option", // text box placeholder and select element
- ".transition > div > span:not([class])", ".label-wrap > span", // collapse panel added by extension
- ".gradio-image>div.float", // image upload description
- ".gradio-file>div.float", // file upload description
- ".gradio-code>div.float", // code editor description
- "#modelmerger_interp_description .output-html", // model merger description
- "#modelmerger_interp_description .gradio-html", // model merger description
- "#lightboxModal span" // image preview lightbox
- ];
-
- const minorSelectors = [
- 'div[data-testid="image"] > div > div', // description of image upload panel
- '#extras_image_batch > div', // description of extras image batch file upload panel
- ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", // output html exclude footer
- '#dynamic-prompting' // dynamic-prompting extension
- ];
-
- // Process major selectors
- majorSelectors.forEach(selector => {
- querySelectorAll(selector).forEach(el => translateEl(el, { deep: true }))
- });
-
- // Process minor selectors
- minorSelectors.forEach(selector => {
- querySelectorAll(selector).forEach(el => translateEl(el, { rich: true }))
- });
-
- logger.timeEnd('Full Page')
-}
+import { getI18n } from "../setup";
+import { createLogger } from "./create-logger";
+import { querySelectorAll } from "./gradio-app";
+import { translateEl } from "./translate-el";
+
+export function translatePage() {
+ if (!getI18n()) return;
+
+ const logger = createLogger();
+ logger.time("Full Page");
+
+ // Define arrays of selectors
+ const majorSelectors = [
+ "label span, fieldset span, button", // major label and button description text
+ "textarea[placeholder], select, option", // text box placeholder and select element
+ ".transition > div > span:not([class])",
+ ".label-wrap > span", // collapse panel added by extension
+ ".gradio-image>div.float", // image upload description
+ ".gradio-file>div.float", // file upload description
+ ".gradio-code>div.float", // code editor description
+ "#modelmerger_interp_description .output-html", // model merger description
+ "#modelmerger_interp_description .gradio-html", // model merger description
+ "#lightboxModal span", // image preview lightbox
+ ];
+
+ const minorSelectors = [
+ 'div[data-testid="image"] > div > div', // description of image upload panel
+ "#extras_image_batch > div", // description of extras image batch file upload panel
+ ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", // output html exclude footer
+ "#dynamic-prompting", // dynamic-prompting extension
+ ];
+
+ // Process major selectors
+ // biome-ignore lint/complexity/noForEach:
+ majorSelectors.forEach((selector) => {
+ // biome-ignore lint/complexity/noForEach:
+ querySelectorAll(selector).forEach((el) => translateEl(el, { deep: true }));
+ });
+
+ // Process minor selectors
+ // biome-ignore lint/complexity/noForEach:
+ minorSelectors.forEach((selector) => {
+ // biome-ignore lint/complexity/noForEach:
+ querySelectorAll(selector).forEach((el) => translateEl(el, { rich: true }));
+ });
+
+ logger.timeEnd("Full Page");
+}
diff --git a/src/lib/translate-el.ts b/src/lib/translate-el.ts
index 91ff871..77423d9 100644
--- a/src/lib/translate-el.ts
+++ b/src/lib/translate-el.ts
@@ -1,48 +1,49 @@
-import { getI18n } from "../setup"
-import { doTranslate } from "./do-translate"
-
-const ignore_selector = [
- '.bilingual__trans_wrapper', // self
- '.resultsFlexContainer', // tag-autocomplete
- '#setting_sd_model_checkpoint select', // model checkpoint
- '#setting_sd_vae select', // vae model
- '#txt2img_styles, #img2txt_styles', // styles select
- '.extra-network-cards .card .actions .name', // extra network cards name
- 'script, style, svg, g, path', // script / style / svg elements
-]
-
-export function translateEl(el, { deep = false, rich = false } = {}) {
- if (!getI18n) return // translation not ready.
- if (el.matches?.(ignore_selector)) return // ignore some elements.
-
- if (el.title) {
- doTranslate(el, el.title, 'title')
- }
-
- if (el.placeholder) {
- doTranslate(el, el.placeholder, 'placeholder')
- }
-
- if (el.tagName === 'OPTION') {
- doTranslate(el, el.textContent, 'option')
- }
-
- if (deep || rich) {
- Array.from(el.childNodes).forEach(node => {
- if ((node as Text).nodeName === '#text') {
- if (rich) {
- doTranslate(node, (node as Text).textContent, 'text')
- return
- }
-
- if (deep) {
- doTranslate(node, (node as Text).textContent, 'element')
- }
- } else if ((node as Text).childNodes.length > 0) {
- translateEl(node, { deep, rich })
- }
- })
- } else {
- doTranslate(el, el.textContent, 'element')
- }
- }
\ No newline at end of file
+import { getI18n } from "../setup";
+import { doTranslate } from "./do-translate";
+
+const ignore_selector = [
+ ".bilingual__trans_wrapper", // self
+ ".resultsFlexContainer", // tag-autocomplete
+ "#setting_sd_model_checkpoint select", // model checkpoint
+ "#setting_sd_vae select", // vae model
+ "#txt2img_styles, #img2txt_styles", // styles select
+ ".extra-network-cards .card .actions .name", // extra network cards name
+ "script, style, svg, g, path", // script / style / svg elements
+];
+
+export function translateEl(el, { deep = false, rich = false } = {}) {
+ if (!getI18n) return; // translation not ready.
+ if (el.matches?.(ignore_selector)) return; // ignore some elements.
+
+ if (el.title) {
+ doTranslate(el, el.title, "title");
+ }
+
+ if (el.placeholder) {
+ doTranslate(el, el.placeholder, "placeholder");
+ }
+
+ if (el.tagName === "OPTION") {
+ doTranslate(el, el.textContent, "option");
+ }
+
+ if (deep || rich) {
+ // biome-ignore lint/complexity/noForEach:
+ Array.from(el.childNodes).forEach((node) => {
+ if ((node as Text).nodeName === "#text") {
+ if (rich) {
+ doTranslate(node, (node as Text).textContent, "text");
+ return;
+ }
+
+ if (deep) {
+ doTranslate(node, (node as Text).textContent, "element");
+ }
+ } else if ((node as Text).childNodes.length > 0) {
+ translateEl(node, { deep, rich });
+ }
+ });
+ } else {
+ doTranslate(el, el.textContent, "element");
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index cccbc6d..8765f9d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,31 +1,31 @@
-import { init } from './init';
-interface I18n {
- [key: string]: string;
-}
-
-interface I18nScope {
- [scope: string]: I18n;
-}
-
-interface ScopedSource {
- [source: string]: string[];
-}
-
-interface Config {
- enabled: boolean;
- file: string;
- dirs: string[];
- order: string;
- enableLogger: boolean;
-}
-
-let i18n: I18n | null = null;
-let i18nRegex: Map = new Map();
-let i18nScope: I18nScope = {};
-let scopedSource: ScopedSource = {};
-let config: Config | null = null;
-
-// DOMContentLoaded イベント発生後に初期化処理を実行
-document.addEventListener('DOMContentLoaded', () => {
- init()
-});
+import { init } from "./init";
+interface I18n {
+ [key: string]: string;
+}
+
+interface I18nScope {
+ [scope: string]: I18n;
+}
+
+interface ScopedSource {
+ [source: string]: string[];
+}
+
+interface Config {
+ enabled: boolean;
+ file: string;
+ dirs: string[];
+ order: string;
+ enableLogger: boolean;
+}
+
+const i18n: I18n | null = null;
+const i18nRegex: Map = new Map();
+const i18nScope: I18nScope = {};
+const scopedSource: ScopedSource = {};
+const config: Config | null = null;
+
+// DOMContentLoaded イベント発生後に初期化処理を実行
+document.addEventListener("DOMContentLoaded", () => {
+ init();
+});
diff --git a/src/setup.ts b/src/setup.ts
index c5579fa..3aa01d2 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -1,99 +1,100 @@
-import { readFile } from "./lib/read-files";
-import { getRegex } from "./lib/get-regax";
-import { createLogger } from "./lib/create-logger";
-import { opts } from "./config/opts";
-import { handleDropdown } from "./lib/handle-dropdown";
-import { translatePage } from "./lib/tranlate-page";
-
-interface Config {
- enabled: boolean;
- file: string;
- dirs: string;
- order: string;
- enableLogger: boolean;
-}
-
-let i18n = null;
-const i18nRegex = new Map();
-const i18nScope = {};
-const scopedSource = {};
-let config: Config | null = null;
-
-export function setup() {
- config = {
- enabled: opts.bilingual_localization_enabled,
- file: opts.bilingual_localization_file,
- dirs: opts.bilingual_localization_dirs,
- order: opts.bilingual_localization_order,
- enableLogger: opts.bilingual_localization_logger,
- };
-
- let { enabled, file, dirs, enableLogger } = config;
-
- if (!enabled || file === "None" || dirs === "None") return;
- const dirsParsed = JSON.parse(dirs);
-
- const logger = createLogger();
- if (enableLogger) {
- logger.init('Bilingual');
- }
- logger.log('Bilingual Localization initialized.');
-
- // Load localization file
- const regex_scope = /^##(?.+)##(?.+)$/; // ##scope##.skey
- i18n = JSON.parse(readFile(dirsParsed[file]), (key, value) => {
- // parse regex translations
- if (key.startsWith('@@')) {
- const regex = getRegex(key.slice(2));
- if (regex instanceof RegExp) {
- i18nRegex.set(regex, value);
- }
- } else {
- const match = key.match(regex_scope);
- if (match && match.groups) {
- // parse scope translations
- let { scope, skey } = match.groups;
-
- if (scope.startsWith('@')) {
- scope = scope.slice(1);
- } else {
- scope = '#' + scope;
- }
-
- if (!scope.length) {
- return value;
- }
-
- i18nScope[scope] ||= {};
- i18nScope[scope][skey] = value;
-
- scopedSource[skey] ||= [];
- scopedSource[skey].push(scope);
- } else {
- return value;
- }
- }
- });
- translatePage()
- handleDropdown()
-}
-
-export function getI18n() {
- return i18n;
-}
-
-export function getI18nRegex() {
- return i18nRegex;
-}
-
-export function getI18nScope() {
- return i18nScope;
-}
-
-export function getScopedSource() {
- return scopedSource;
-}
-
-export function getConfig() {
- return config;
-}
+import { opts } from "./config/opts";
+import { createLogger } from "./lib/create-logger";
+import { getRegex } from "./lib/get-regax";
+import { handleDropdown } from "./lib/handle-dropdown";
+import { readFile } from "./lib/read-files";
+import { translatePage } from "./lib/tranlate-page";
+
+interface Config {
+ enabled: boolean;
+ file: string;
+ dirs: string;
+ order: string;
+ enableLogger: boolean;
+}
+
+let i18n = null;
+// biome-ignore lint/suspicious/noExplicitAny:
+const i18nRegex = new Map();
+const i18nScope = {};
+const scopedSource = {};
+let config: Config | null = null;
+
+export function setup() {
+ config = {
+ enabled: opts.bilingual_localization_enabled,
+ file: opts.bilingual_localization_file,
+ dirs: opts.bilingual_localization_dirs,
+ order: opts.bilingual_localization_order,
+ enableLogger: opts.bilingual_localization_logger,
+ };
+
+ const { enabled, file, dirs, enableLogger } = config;
+
+ if (!enabled || file === "None" || dirs === "None") return;
+ const dirsParsed = JSON.parse(dirs);
+
+ const logger = createLogger();
+ if (enableLogger) {
+ logger.init("Bilingual");
+ }
+ logger.log("Bilingual Localization initialized.");
+
+ // Load localization file
+ const regex_scope = /^##(?.+)##(?.+)$/; // ##scope##.skey
+ i18n = JSON.parse(readFile(dirsParsed[file]), (key, value) => {
+ // parse regex translations
+ if (key.startsWith("@@")) {
+ const regex = getRegex(key.slice(2));
+ if (regex instanceof RegExp) {
+ i18nRegex.set(regex, value);
+ }
+ } else {
+ const match = key.match(regex_scope);
+ if (match?.groups) {
+ // parse scope translations
+ let { scope, skey } = match.groups;
+
+ if (scope.startsWith("@")) {
+ scope = scope.slice(1);
+ } else {
+ scope = `#${scope}`;
+ }
+
+ if (!scope.length) {
+ return value;
+ }
+
+ i18nScope[scope] ||= {};
+ i18nScope[scope][skey] = value;
+
+ scopedSource[skey] ||= [];
+ scopedSource[skey].push(scope);
+ } else {
+ return value;
+ }
+ }
+ });
+ translatePage();
+ handleDropdown();
+}
+
+export function getI18n() {
+ return i18n;
+}
+
+export function getI18nRegex() {
+ return i18nRegex;
+}
+
+export function getI18nScope() {
+ return i18nScope;
+}
+
+export function getScopedSource() {
+ return scopedSource;
+}
+
+export function getConfig() {
+ return config;
+}
diff --git a/src/types/opts.d.ts b/src/types/opts.d.ts
index 35a0fba..164aca8 100644
--- a/src/types/opts.d.ts
+++ b/src/types/opts.d.ts
@@ -1,8 +1,7 @@
-export interface Opts {
- bilingual_localization_enabled: boolean;
- bilingual_localization_logger: boolean;
- bilingual_localization_file: string;
- bilingual_localization_dirs: string;
- bilingual_localization_order: string;
- }
-
\ No newline at end of file
+export interface Opts {
+ bilingual_localization_enabled: boolean;
+ bilingual_localization_logger: boolean;
+ bilingual_localization_file: string;
+ bilingual_localization_dirs: string;
+ bilingual_localization_order: string;
+}
From f454839f5c74b33d94e3a689dd7e160e921555a2 Mon Sep 17 00:00:00 2001
From: Katsuyuki-Karasawa <4ranci0ne@gmail.com>
Date: Sun, 4 Aug 2024 01:42:25 +0900
Subject: [PATCH 3/5] build
---
javascript/bilingual_localization.js | 94 ++++++++++++++++++++++++++++
1 file changed, 94 insertions(+)
create mode 100644 javascript/bilingual_localization.js
diff --git a/javascript/bilingual_localization.js b/javascript/bilingual_localization.js
new file mode 100644
index 0000000..c16947a
--- /dev/null
+++ b/javascript/bilingual_localization.js
@@ -0,0 +1,94 @@
+var F={bilingual_localization_enabled:!0,bilingual_localization_logger:!1,bilingual_localization_file:"None",bilingual_localization_dirs:"{}",bilingual_localization_order:"Translation First"};function J(){const f=new Map,w={badge:!0,label:"Logger",enable:!1};return new Proxy(console,{get:(z,R)=>{if(R==="init")return(_)=>{w.label=_,w.enable=!0};if(!(R in z))return;return(..._)=>{if(!w.enable)return;let q=["#39cfe1","#006cab"],O,G;switch(R){case"error":q=["#f70000","#a70000"];break;case"warn":q=["#f7b500","#b58400"];break;case"time":if(O=_[0],f.has(O))console.warn(`Timer '${O}' already exists`);else f.set(O,performance.now());return;case"timeEnd":if(O=_[0],G=f.get(O),G===void 0)console.warn(`Timer '${O}' does not exist`);else f.delete(O),console.log(`${O}: ${performance.now()-G} ms`);return;case"groupEnd":w.badge=!0;break}const H=w.badge?[`%c${w.label}`,`color: #fff; background: linear-gradient(180deg, ${q[0]}, ${q[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`]:[];if(z[R](...H,..._),R==="group"||R==="groupCollapsed")w.badge=!1}}})}function D(f){try{const w=f.trim();if(!w.startsWith("/")||w.split("/").length<3){const q=w.replace(/[.*+\-?^${}()|[\]\\]/g,"\\$&");return new RegExp(q)}const z=w.lastIndexOf("/"),R=w.slice(1,z),_=w.slice(z+1);return new RegExp(R,_)}catch(w){return null}}function P(f,w,z,R){f.addEventListener(w,(_)=>{let q=_.target;while(q!==f){if(q.matches(z))R.call(q,_);q=q.parentNode}})}function L(){const f=document.getElementsByTagName("gradio-app"),w=f.length===0?document:f[0];if(w!==document)w.getElementById=(z)=>document.getElementById(z);return w.shadowRoot?w.shadowRoot:w}function U(...f){return L()?.querySelectorAll(...f)||new NodeList}function v(){P(L(),"mousedown","ul.options .item",(f)=>{const{target:w}=f;if(!w.classList.contains("item")){w.closest(".item").dispatchEvent(new Event("mousedown",{bubbles:!0}));return}const z=w.dataset.value,R=w?.closest(".wrap")?.querySelector(".wrap-inner .single-select");if(z&&R)R.title=titles?.[z]||"",R.textContent="__biligual__will_be_replaced__",X(R,z,"element")})}function I(f){const w=new XMLHttpRequest;return w.open("GET",`file=${f}`,!1),w.send(null),w.responseText}function j(f,{deep:w=!1,rich:z=!1}={}){if(!B)return;if(f.matches?.(S))return;if(f.title)X(f,f.title,"title");if(f.placeholder)X(f,f.placeholder,"placeholder");if(f.tagName==="OPTION")X(f,f.textContent,"option");if(w||z)Array.from(f.childNodes).forEach((R)=>{if(R.nodeName==="#text"){if(z){X(R,R.textContent,"text");return}if(w)X(R,R.textContent,"element")}else if(R.childNodes.length>0)j(R,{deep:w,rich:z})});else X(f,f.textContent,"element")}var S=[".bilingual__trans_wrapper",".resultsFlexContainer","#setting_sd_model_checkpoint select","#setting_sd_vae select","#txt2img_styles, #img2txt_styles",".extra-network-cards .card .actions .name","script, style, svg, g, path"];function M(){if(!B())return;const f=J();f.time("Full Page");const w=["label span, fieldset span, button","textarea[placeholder], select, option",".transition > div > span:not([class])",".label-wrap > span",".gradio-image>div.float",".gradio-file>div.float",".gradio-code>div.float","#modelmerger_interp_description .output-html","#modelmerger_interp_description .gradio-html","#lightboxModal span"],z=['div[data-testid="image"] > div > div',"#extras_image_batch > div",".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown","#dynamic-prompting"];w.forEach((R)=>{U(R).forEach((_)=>j(_,{deep:!0}))}),z.forEach((R)=>{U(R).forEach((_)=>j(_,{rich:!0}))}),f.timeEnd("Full Page")}function A(){Z={enabled:F.bilingual_localization_enabled,file:F.bilingual_localization_file,dirs:F.bilingual_localization_dirs,order:F.bilingual_localization_order,enableLogger:F.bilingual_localization_logger};const{enabled:f,file:w,dirs:z,enableLogger:R}=Z;if(!f||w==="None"||z==="None")return;const _=JSON.parse(z),q=J();if(R)q.init("Bilingual");q.log("Bilingual Localization initialized.");const O=/^##(?.+)##(?.+)$/;N=JSON.parse(I(_[w]),(G,H)=>{if(G.startsWith("@@")){const $=D(G.slice(2));if($ instanceof RegExp)T.set($,H)}else{const $=G.match(O);if($?.groups){let{scope:W,skey:Q}=$.groups;if(W.startsWith("@"))W=W.slice(1);else W=`#${W}`;if(!W.length)return H;V[W]||={},V[W][Q]=H,Y[Q]||=[],Y[Q].push(W)}else return H}}),M(),v()}function B(){return N}function k(){return T}function b(){return V}function E(){return Y}function K(){return Z}var N=null,T=new Map,V={},Y={},Z=null;function C(f){const w=k();for(let[z,R]of w.entries())if(z instanceof RegExp){if(z.test(f))return J().log("regex",z,f,R),f.replace(z,R)}else console.warn("Expected regex to be an instance of RegExp, but it was a string.");return f}function x(f){return f.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function h(f){const w=document.createElement("template");return w.insertAdjacentHTML("afterbegin",f),w.firstElementChild}function X(f,w,z){if(!B)return;let R=w.trim();if(!R)return;if(g.test(R))return;if(c.test(R))return;let _=B[R]||C(R);const q=E[R];if(q){console.log("scope",f,R,q);for(let G of q)if(f.parentElement.closest(G)){_=b[G][R];break}}if(!_||R===_){if(f.textContent==="__biligual__will_be_replaced__")f.textContent=R;if(f.nextSibling?.className==="bilingual__trans_wrapper")f.nextSibling.remove();return}if(K()?.order==="Original First")[R,_]=[_,R];switch(z){case"text":f.textContent=_;break;case"element":{const G=`${x(_)}${x(R)}
`,H=h(G);if(f.hasChildNodes()){const $=Array.from(f.childNodes).find((W)=>W.nodeType===Node.TEXT_NODE&&W.textContent?.trim()===R||W.textContent?.trim()==="__bilingual__will_be_replaced__");if($){if($.textContent="",$.nextSibling?.nodeType===Node.ELEMENT_NODE&&$.nextSibling.className==="bilingual__trans_wrapper")$.nextSibling.remove();if($.parentNode&&H)$.parentNode.insertBefore(H,$.nextSibling)}}else{if(f.textContent="",f.nextSibling?.nodeType===Node.ELEMENT_NODE&&f.nextSibling.className==="bilingual__trans_wrapper")f.nextSibling.remove();if(f.parentNode&&H)f.parentNode.insertBefore(H,f.nextSibling)}break}case"option":f.textContent=`${_} (${R})`;break;case"title":f.title=`${_}\n${R}`;break;case"placeholder":f.placeholder=`${_}\n\n${R}`;break;default:return _}}var g=/^[\.\d]+$/,c=/[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;function y(){const f=document.createElement("style");if(f.textContent)f.textContent=u;else f.appendChild(document.createTextNode(u));L().appendChild(f);let w=!1,z=0;new MutationObserver((_)=>{if(window.localization&&Object.keys(window.localization).length)return;if(Object.keys(F).length===0)return;let q=0;const O=performance.now();for(let H of _)if(H.type==="characterData"){if(H.target?.parentElement?.parentElement?.tagName==="LABEL")j(H.target)}else if(H.type==="attributes")q++,j(H.target);else H.addedNodes.forEach(($)=>{if($ instanceof Element&&$.className==="bilingual__trans_wrapper")return;if(q++,$.nodeType===1&&$ instanceof Element&&/(output|gradio)-(html|markdown)/.test($.className))j($,{rich:!0});else if($.nodeType===3)X($,$.textContent,"text");else j($,{deep:!0})});if(q>0)J().info(`UI Update #${z++}: ${performance.now()-O} ms, ${q} nodes`,_);if(w)return;if(B())return;w=!0,A()}).observe(L(),{characterData:!0,childList:!0,subtree:!0,attributes:!0,attributeFilter:["title","placeholder"]})}var u=`
+ .bilingual__trans_wrapper {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ .bilingual__trans_wrapper em {
+ font-style: normal;
+ }
+
+ #txtimg_hr_finalres .bilingual__trans_wrapper em,
+ #tab_ti .output-html .bilingual__trans_wrapper em,
+ #tab_ti .gradio-html .bilingual__trans_wrapper em,
+ #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
+ #available_extensions .extension-tag .bilingual__trans_wrapper em,
+ #available_extensions .date_added .bilingual__trans_wrapper em,
+ #available_extensions+p>.bilingual__trans_wrapper em,
+ .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
+ display: none;
+ }
+
+ #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
+ label>span>.bilingual__trans_wrapper,
+ fieldset>span>.bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper,
+ .w-full>span>.bilingual__trans_wrapper,
+ .context-menu-items .bilingual__trans_wrapper,
+ .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
+ .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
+ .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
+ .output-markdown .bilingual__trans_wrapper,
+ .gradio-markdown .bilingual__trans_wrapper,
+ .gradio-image>div.float .bilingual__trans_wrapper,
+ .gradio-file>div.float .bilingual__trans_wrapper,
+ .gradio-code>div.float .bilingual__trans_wrapper,
+ .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
+ #dynamic-prompting .bilingual__trans_wrapper
+ {
+ font-size: 12px;
+ align-items: flex-start;
+ }
+
+ #extensions label .bilingual__trans_wrapper,
+ #available_extensions td .bilingual__trans_wrapper,
+ .label-wrap>span>.bilingual__trans_wrapper {
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ .label-wrap>span:first-of-type {
+ font-size: 13px;
+ line-height: 1;
+ }
+
+ #txt2img_script_container > div {
+ margin-top: var(--layout-gap, 12px);
+ }
+
+ textarea::placeholder {
+ line-height: 1;
+ padding: 4px 0;
+ }
+
+ label>span {
+ line-height: 1;
+ }
+
+ div[data-testid="image"] .start-prompt {
+ background-color: rgba(255, 255, 255, .6);
+ color: #222;
+ transition: opacity .2s ease-in-out;
+ }
+ div[data-testid="image"]:hover .start-prompt {
+ opacity: 0;
+ }
+
+ .label-wrap > span.icon {
+ width: 1em;
+ height: 1em;
+ transform-origin: center center;
+ }
+
+ .gradio-dropdown ul.options li.item {
+ padding: 0.3em 0.4em !important;
+ }
+
+ /* Posex extension */
+ .posex_bg {
+ white-space: nowrap;
+ }
+ `;document.addEventListener("DOMContentLoaded",()=>{y()});
From e0553e53cf3d5666ca5e2f6159654284416a51e0 Mon Sep 17 00:00:00 2001
From: Katsuyuki-Karasawa <4ranci0ne@gmail.com>
Date: Sun, 4 Aug 2024 01:54:40 +0900
Subject: [PATCH 4/5] remove minify
---
README.md | 2 +-
javascript/bilingual_localization.js | 465 ++++++++++++++++++++++++++-
2 files changed, 464 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 8bad01b..3a0b805 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
bunx @biomejs/biome check --config-path=./biome.json
# build
-bun build ./src/main.ts --outfile ./javascript/bilingual_localization.js --minify
+bun build ./src/main.ts --outfile ./javascript/bilingual_localization.js
```
diff --git a/javascript/bilingual_localization.js b/javascript/bilingual_localization.js
index c16947a..4ff5315 100644
--- a/javascript/bilingual_localization.js
+++ b/javascript/bilingual_localization.js
@@ -1,4 +1,459 @@
-var F={bilingual_localization_enabled:!0,bilingual_localization_logger:!1,bilingual_localization_file:"None",bilingual_localization_dirs:"{}",bilingual_localization_order:"Translation First"};function J(){const f=new Map,w={badge:!0,label:"Logger",enable:!1};return new Proxy(console,{get:(z,R)=>{if(R==="init")return(_)=>{w.label=_,w.enable=!0};if(!(R in z))return;return(..._)=>{if(!w.enable)return;let q=["#39cfe1","#006cab"],O,G;switch(R){case"error":q=["#f70000","#a70000"];break;case"warn":q=["#f7b500","#b58400"];break;case"time":if(O=_[0],f.has(O))console.warn(`Timer '${O}' already exists`);else f.set(O,performance.now());return;case"timeEnd":if(O=_[0],G=f.get(O),G===void 0)console.warn(`Timer '${O}' does not exist`);else f.delete(O),console.log(`${O}: ${performance.now()-G} ms`);return;case"groupEnd":w.badge=!0;break}const H=w.badge?[`%c${w.label}`,`color: #fff; background: linear-gradient(180deg, ${q[0]}, ${q[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`]:[];if(z[R](...H,..._),R==="group"||R==="groupCollapsed")w.badge=!1}}})}function D(f){try{const w=f.trim();if(!w.startsWith("/")||w.split("/").length<3){const q=w.replace(/[.*+\-?^${}()|[\]\\]/g,"\\$&");return new RegExp(q)}const z=w.lastIndexOf("/"),R=w.slice(1,z),_=w.slice(z+1);return new RegExp(R,_)}catch(w){return null}}function P(f,w,z,R){f.addEventListener(w,(_)=>{let q=_.target;while(q!==f){if(q.matches(z))R.call(q,_);q=q.parentNode}})}function L(){const f=document.getElementsByTagName("gradio-app"),w=f.length===0?document:f[0];if(w!==document)w.getElementById=(z)=>document.getElementById(z);return w.shadowRoot?w.shadowRoot:w}function U(...f){return L()?.querySelectorAll(...f)||new NodeList}function v(){P(L(),"mousedown","ul.options .item",(f)=>{const{target:w}=f;if(!w.classList.contains("item")){w.closest(".item").dispatchEvent(new Event("mousedown",{bubbles:!0}));return}const z=w.dataset.value,R=w?.closest(".wrap")?.querySelector(".wrap-inner .single-select");if(z&&R)R.title=titles?.[z]||"",R.textContent="__biligual__will_be_replaced__",X(R,z,"element")})}function I(f){const w=new XMLHttpRequest;return w.open("GET",`file=${f}`,!1),w.send(null),w.responseText}function j(f,{deep:w=!1,rich:z=!1}={}){if(!B)return;if(f.matches?.(S))return;if(f.title)X(f,f.title,"title");if(f.placeholder)X(f,f.placeholder,"placeholder");if(f.tagName==="OPTION")X(f,f.textContent,"option");if(w||z)Array.from(f.childNodes).forEach((R)=>{if(R.nodeName==="#text"){if(z){X(R,R.textContent,"text");return}if(w)X(R,R.textContent,"element")}else if(R.childNodes.length>0)j(R,{deep:w,rich:z})});else X(f,f.textContent,"element")}var S=[".bilingual__trans_wrapper",".resultsFlexContainer","#setting_sd_model_checkpoint select","#setting_sd_vae select","#txt2img_styles, #img2txt_styles",".extra-network-cards .card .actions .name","script, style, svg, g, path"];function M(){if(!B())return;const f=J();f.time("Full Page");const w=["label span, fieldset span, button","textarea[placeholder], select, option",".transition > div > span:not([class])",".label-wrap > span",".gradio-image>div.float",".gradio-file>div.float",".gradio-code>div.float","#modelmerger_interp_description .output-html","#modelmerger_interp_description .gradio-html","#lightboxModal span"],z=['div[data-testid="image"] > div > div',"#extras_image_batch > div",".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown","#dynamic-prompting"];w.forEach((R)=>{U(R).forEach((_)=>j(_,{deep:!0}))}),z.forEach((R)=>{U(R).forEach((_)=>j(_,{rich:!0}))}),f.timeEnd("Full Page")}function A(){Z={enabled:F.bilingual_localization_enabled,file:F.bilingual_localization_file,dirs:F.bilingual_localization_dirs,order:F.bilingual_localization_order,enableLogger:F.bilingual_localization_logger};const{enabled:f,file:w,dirs:z,enableLogger:R}=Z;if(!f||w==="None"||z==="None")return;const _=JSON.parse(z),q=J();if(R)q.init("Bilingual");q.log("Bilingual Localization initialized.");const O=/^##(?.+)##(?.+)$/;N=JSON.parse(I(_[w]),(G,H)=>{if(G.startsWith("@@")){const $=D(G.slice(2));if($ instanceof RegExp)T.set($,H)}else{const $=G.match(O);if($?.groups){let{scope:W,skey:Q}=$.groups;if(W.startsWith("@"))W=W.slice(1);else W=`#${W}`;if(!W.length)return H;V[W]||={},V[W][Q]=H,Y[Q]||=[],Y[Q].push(W)}else return H}}),M(),v()}function B(){return N}function k(){return T}function b(){return V}function E(){return Y}function K(){return Z}var N=null,T=new Map,V={},Y={},Z=null;function C(f){const w=k();for(let[z,R]of w.entries())if(z instanceof RegExp){if(z.test(f))return J().log("regex",z,f,R),f.replace(z,R)}else console.warn("Expected regex to be an instance of RegExp, but it was a string.");return f}function x(f){return f.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function h(f){const w=document.createElement("template");return w.insertAdjacentHTML("afterbegin",f),w.firstElementChild}function X(f,w,z){if(!B)return;let R=w.trim();if(!R)return;if(g.test(R))return;if(c.test(R))return;let _=B[R]||C(R);const q=E[R];if(q){console.log("scope",f,R,q);for(let G of q)if(f.parentElement.closest(G)){_=b[G][R];break}}if(!_||R===_){if(f.textContent==="__biligual__will_be_replaced__")f.textContent=R;if(f.nextSibling?.className==="bilingual__trans_wrapper")f.nextSibling.remove();return}if(K()?.order==="Original First")[R,_]=[_,R];switch(z){case"text":f.textContent=_;break;case"element":{const G=`${x(_)}${x(R)}
`,H=h(G);if(f.hasChildNodes()){const $=Array.from(f.childNodes).find((W)=>W.nodeType===Node.TEXT_NODE&&W.textContent?.trim()===R||W.textContent?.trim()==="__bilingual__will_be_replaced__");if($){if($.textContent="",$.nextSibling?.nodeType===Node.ELEMENT_NODE&&$.nextSibling.className==="bilingual__trans_wrapper")$.nextSibling.remove();if($.parentNode&&H)$.parentNode.insertBefore(H,$.nextSibling)}}else{if(f.textContent="",f.nextSibling?.nodeType===Node.ELEMENT_NODE&&f.nextSibling.className==="bilingual__trans_wrapper")f.nextSibling.remove();if(f.parentNode&&H)f.parentNode.insertBefore(H,f.nextSibling)}break}case"option":f.textContent=`${_} (${R})`;break;case"title":f.title=`${_}\n${R}`;break;case"placeholder":f.placeholder=`${_}\n\n${R}`;break;default:return _}}var g=/^[\.\d]+$/,c=/[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;function y(){const f=document.createElement("style");if(f.textContent)f.textContent=u;else f.appendChild(document.createTextNode(u));L().appendChild(f);let w=!1,z=0;new MutationObserver((_)=>{if(window.localization&&Object.keys(window.localization).length)return;if(Object.keys(F).length===0)return;let q=0;const O=performance.now();for(let H of _)if(H.type==="characterData"){if(H.target?.parentElement?.parentElement?.tagName==="LABEL")j(H.target)}else if(H.type==="attributes")q++,j(H.target);else H.addedNodes.forEach(($)=>{if($ instanceof Element&&$.className==="bilingual__trans_wrapper")return;if(q++,$.nodeType===1&&$ instanceof Element&&/(output|gradio)-(html|markdown)/.test($.className))j($,{rich:!0});else if($.nodeType===3)X($,$.textContent,"text");else j($,{deep:!0})});if(q>0)J().info(`UI Update #${z++}: ${performance.now()-O} ms, ${q} nodes`,_);if(w)return;if(B())return;w=!0,A()}).observe(L(),{characterData:!0,childList:!0,subtree:!0,attributes:!0,attributeFilter:["title","placeholder"]})}var u=`
+// src/config/opts.ts
+var opts = {
+ bilingual_localization_enabled: true,
+ bilingual_localization_logger: false,
+ bilingual_localization_file: "None",
+ bilingual_localization_dirs: "{}",
+ bilingual_localization_order: "Translation First"
+};
+
+// src/lib/create-logger.ts
+function createLogger() {
+ const loggerTimerMap = new Map;
+ const loggerConf = { badge: true, label: "Logger", enable: false };
+ return new Proxy(console, {
+ get: (target, propKey) => {
+ if (propKey === "init") {
+ return (label) => {
+ loggerConf.label = label;
+ loggerConf.enable = true;
+ };
+ }
+ if (!(propKey in target))
+ return;
+ return (...args) => {
+ if (!loggerConf.enable)
+ return;
+ let color = ["#39cfe1", "#006cab"];
+ let label;
+ let start;
+ switch (propKey) {
+ case "error":
+ color = ["#f70000", "#a70000"];
+ break;
+ case "warn":
+ color = ["#f7b500", "#b58400"];
+ break;
+ case "time":
+ label = args[0];
+ if (loggerTimerMap.has(label)) {
+ console.warn(`Timer '${label}' already exists`);
+ } else {
+ loggerTimerMap.set(label, performance.now());
+ }
+ return;
+ case "timeEnd":
+ label = args[0];
+ start = loggerTimerMap.get(label);
+ if (start === undefined) {
+ console.warn(`Timer '${label}' does not exist`);
+ } else {
+ loggerTimerMap.delete(label);
+ console.log(`${label}: ${performance.now() - start} ms`);
+ }
+ return;
+ case "groupEnd":
+ loggerConf.badge = true;
+ break;
+ }
+ const badge = loggerConf.badge ? [
+ `%c${loggerConf.label}`,
+ `color: #fff; background: linear-gradient(180deg, ${color[0]}, ${color[1]}); text-shadow: 0px 0px 1px #0003; padding: 3px 5px; border-radius: 4px;`
+ ] : [];
+ target[propKey](...badge, ...args);
+ if (propKey === "group" || propKey === "groupCollapsed") {
+ loggerConf.badge = false;
+ }
+ };
+ }
+ });
+}
+
+// src/lib/get-regax.ts
+function getRegex(regexString) {
+ try {
+ const trimmedRegexString = regexString.trim();
+ if (!trimmedRegexString.startsWith("/") || trimmedRegexString.split("/").length < 3) {
+ const escapedRegexString = trimmedRegexString.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ return new RegExp(escapedRegexString);
+ }
+ const lastSlashIndex = trimmedRegexString.lastIndexOf("/");
+ const regexPattern = trimmedRegexString.slice(1, lastSlashIndex);
+ const regexFlags = trimmedRegexString.slice(lastSlashIndex + 1);
+ return new RegExp(regexPattern, regexFlags);
+ } catch (e) {
+ return null;
+ }
+}
+
+// src/lib/delegate-event.ts
+function delegateEvent(parent, eventType, selector, handler) {
+ parent.addEventListener(eventType, (event) => {
+ let target = event.target;
+ while (target !== parent) {
+ if (target.matches(selector)) {
+ handler.call(target, event);
+ }
+ target = target.parentNode;
+ }
+ });
+}
+
+// src/lib/gradio-app.ts
+function gradioApp() {
+ const elems = document.getElementsByTagName("gradio-app");
+ const elem = elems.length === 0 ? document : elems[0];
+ if (elem !== document) {
+ elem.getElementById = (id) => document.getElementById(id);
+ }
+ return elem.shadowRoot ? elem.shadowRoot : elem;
+}
+function querySelectorAll(...args) {
+ const nodeList = gradioApp()?.querySelectorAll(...args);
+ return nodeList || new NodeList;
+}
+
+// src/lib/handle-dropdown.ts
+function handleDropdown() {
+ delegateEvent(gradioApp(), "mousedown", "ul.options .item", (event) => {
+ const { target } = event;
+ if (!target.classList.contains("item")) {
+ target.closest(".item").dispatchEvent(new Event("mousedown", { bubbles: true }));
+ return;
+ }
+ const source = target.dataset.value;
+ const $labelEl = target?.closest(".wrap")?.querySelector(".wrap-inner .single-select");
+ if (source && $labelEl) {
+ $labelEl.title = titles?.[source] || "";
+ $labelEl.textContent = "__biligual__will_be_replaced__";
+ doTranslate($labelEl, source, "element");
+ }
+ });
+}
+
+// src/lib/read-files.ts
+function readFile(filePath) {
+ const request = new XMLHttpRequest;
+ request.open("GET", `file=${filePath}`, false);
+ request.send(null);
+ return request.responseText;
+}
+
+// src/lib/translate-el.ts
+function translateEl(el, { deep = false, rich = false } = {}) {
+ if (!getI18n)
+ return;
+ if (el.matches?.(ignore_selector))
+ return;
+ if (el.title) {
+ doTranslate(el, el.title, "title");
+ }
+ if (el.placeholder) {
+ doTranslate(el, el.placeholder, "placeholder");
+ }
+ if (el.tagName === "OPTION") {
+ doTranslate(el, el.textContent, "option");
+ }
+ if (deep || rich) {
+ Array.from(el.childNodes).forEach((node) => {
+ if (node.nodeName === "#text") {
+ if (rich) {
+ doTranslate(node, node.textContent, "text");
+ return;
+ }
+ if (deep) {
+ doTranslate(node, node.textContent, "element");
+ }
+ } else if (node.childNodes.length > 0) {
+ translateEl(node, { deep, rich });
+ }
+ });
+ } else {
+ doTranslate(el, el.textContent, "element");
+ }
+}
+var ignore_selector = [
+ ".bilingual__trans_wrapper",
+ ".resultsFlexContainer",
+ "#setting_sd_model_checkpoint select",
+ "#setting_sd_vae select",
+ "#txt2img_styles, #img2txt_styles",
+ ".extra-network-cards .card .actions .name",
+ "script, style, svg, g, path"
+];
+
+// src/lib/tranlate-page.ts
+function translatePage() {
+ if (!getI18n())
+ return;
+ const logger = createLogger();
+ logger.time("Full Page");
+ const majorSelectors = [
+ "label span, fieldset span, button",
+ "textarea[placeholder], select, option",
+ ".transition > div > span:not([class])",
+ ".label-wrap > span",
+ ".gradio-image>div.float",
+ ".gradio-file>div.float",
+ ".gradio-code>div.float",
+ "#modelmerger_interp_description .output-html",
+ "#modelmerger_interp_description .gradio-html",
+ "#lightboxModal span"
+ ];
+ const minorSelectors = [
+ 'div[data-testid="image"] > div > div',
+ "#extras_image_batch > div",
+ ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown",
+ "#dynamic-prompting"
+ ];
+ majorSelectors.forEach((selector) => {
+ querySelectorAll(selector).forEach((el) => translateEl(el, { deep: true }));
+ });
+ minorSelectors.forEach((selector) => {
+ querySelectorAll(selector).forEach((el) => translateEl(el, { rich: true }));
+ });
+ logger.timeEnd("Full Page");
+}
+
+// src/setup.ts
+function setup3() {
+ config = {
+ enabled: opts.bilingual_localization_enabled,
+ file: opts.bilingual_localization_file,
+ dirs: opts.bilingual_localization_dirs,
+ order: opts.bilingual_localization_order,
+ enableLogger: opts.bilingual_localization_logger
+ };
+ const { enabled, file, dirs, enableLogger } = config;
+ if (!enabled || file === "None" || dirs === "None")
+ return;
+ const dirsParsed = JSON.parse(dirs);
+ const logger = createLogger();
+ if (enableLogger) {
+ logger.init("Bilingual");
+ }
+ logger.log("Bilingual Localization initialized.");
+ const regex_scope = /^##(?.+)##(?.+)$/;
+ i18n = JSON.parse(readFile(dirsParsed[file]), (key, value) => {
+ if (key.startsWith("@@")) {
+ const regex = getRegex(key.slice(2));
+ if (regex instanceof RegExp) {
+ i18nRegex.set(regex, value);
+ }
+ } else {
+ const match = key.match(regex_scope);
+ if (match?.groups) {
+ let { scope, skey } = match.groups;
+ if (scope.startsWith("@")) {
+ scope = scope.slice(1);
+ } else {
+ scope = `#${scope}`;
+ }
+ if (!scope.length) {
+ return value;
+ }
+ i18nScope[scope] ||= {};
+ i18nScope[scope][skey] = value;
+ scopedSource[skey] ||= [];
+ scopedSource[skey].push(scope);
+ } else {
+ return value;
+ }
+ }
+ });
+ translatePage();
+ handleDropdown();
+}
+function getI18n() {
+ return i18n;
+}
+function getI18nRegex() {
+ return i18nRegex;
+}
+function getI18nScope() {
+ return i18nScope;
+}
+function getScopedSource() {
+ return scopedSource;
+}
+function getConfig() {
+ return config;
+}
+var i18n = null;
+var i18nRegex = new Map;
+var i18nScope = {};
+var scopedSource = {};
+var config = null;
+
+// src/lib/check-regax.ts
+function checkRegex(source) {
+ const i18nRegex2 = getI18nRegex();
+ for (const [regex, value] of i18nRegex2.entries()) {
+ if (regex instanceof RegExp) {
+ if (regex.test(source)) {
+ const logger = createLogger();
+ logger.log("regex", regex, source, value);
+ return source.replace(regex, value);
+ }
+ } else {
+ console.warn("Expected regex to be an instance of RegExp, but it was a string.");
+ }
+ }
+ return source;
+}
+
+// src/lib/html-encode.ts
+function htmlEncode(htmlStr) {
+ return htmlStr.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
+}
+
+// src/lib/parse-html-string-to-element.ts
+function parseHtmlStringToElement(htmlStr) {
+ const template = document.createElement("template");
+ template.insertAdjacentHTML("afterbegin", htmlStr);
+ return template.firstElementChild;
+}
+
+// src/lib/do-translate.ts
+function doTranslate(el, source, type) {
+ if (!getI18n)
+ return;
+ let trimmedSource = source.trim();
+ if (!trimmedSource)
+ return;
+ if (re_num.test(trimmedSource))
+ return;
+ if (re_emoji.test(trimmedSource))
+ return;
+ let translation = getI18n[trimmedSource] || checkRegex(trimmedSource);
+ const scopes = getScopedSource[trimmedSource];
+ if (scopes) {
+ console.log("scope", el, trimmedSource, scopes);
+ for (const scope of scopes) {
+ if (el.parentElement.closest(scope)) {
+ translation = getI18nScope[scope][trimmedSource];
+ break;
+ }
+ }
+ }
+ if (!translation || trimmedSource === translation) {
+ if (el.textContent === "__biligual__will_be_replaced__")
+ el.textContent = trimmedSource;
+ if (el.nextSibling?.className === "bilingual__trans_wrapper")
+ el.nextSibling.remove();
+ return;
+ }
+ const config2 = getConfig();
+ if (config2?.order === "Original First") {
+ [trimmedSource, translation] = [translation, trimmedSource];
+ }
+ switch (type) {
+ case "text":
+ el.textContent = translation;
+ break;
+ case "element": {
+ const htmlStr = `${htmlEncode(translation)}${htmlEncode(trimmedSource)}
`;
+ const htmlEl = parseHtmlStringToElement(htmlStr);
+ if (el.hasChildNodes()) {
+ const textNode = Array.from(el.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() === trimmedSource || node.textContent?.trim() === "__bilingual__will_be_replaced__");
+ if (textNode) {
+ textNode.textContent = "";
+ if (textNode.nextSibling?.nodeType === Node.ELEMENT_NODE && textNode.nextSibling.className === "bilingual__trans_wrapper") {
+ textNode.nextSibling.remove();
+ }
+ if (textNode.parentNode && htmlEl) {
+ textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling);
+ }
+ }
+ } else {
+ el.textContent = "";
+ if (el.nextSibling?.nodeType === Node.ELEMENT_NODE && el.nextSibling.className === "bilingual__trans_wrapper") {
+ el.nextSibling.remove();
+ }
+ if (el.parentNode && htmlEl) {
+ el.parentNode.insertBefore(htmlEl, el.nextSibling);
+ }
+ }
+ break;
+ }
+ case "option":
+ el.textContent = `${translation} (${trimmedSource})`;
+ break;
+ case "title":
+ el.title = `${translation}\n${trimmedSource}`;
+ break;
+ case "placeholder":
+ el.placeholder = `${translation}\n\n${trimmedSource}`;
+ break;
+ default:
+ return translation;
+ }
+}
+var re_num = /^[\.\d]+$/;
+var re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;
+
+// src/init.ts
+function init() {
+ const styleEl = document.createElement("style");
+ if (styleEl.textContent) {
+ styleEl.textContent = customCSS;
+ } else {
+ styleEl.appendChild(document.createTextNode(customCSS));
+ }
+ gradioApp().appendChild(styleEl);
+ let loaded = false;
+ let _count = 0;
+ const observer = new MutationObserver((mutations) => {
+ if (window.localization && Object.keys(window.localization).length)
+ return;
+ if (Object.keys(opts).length === 0)
+ return;
+ let _nodesCount = 0;
+ const _now = performance.now();
+ for (const mutation of mutations) {
+ if (mutation.type === "characterData") {
+ if (mutation.target?.parentElement?.parentElement?.tagName === "LABEL") {
+ translateEl(mutation.target);
+ }
+ } else if (mutation.type === "attributes") {
+ _nodesCount++;
+ translateEl(mutation.target);
+ } else {
+ mutation.addedNodes.forEach((node) => {
+ if (node instanceof Element && node.className === "bilingual__trans_wrapper")
+ return;
+ _nodesCount++;
+ if (node.nodeType === 1 && node instanceof Element && /(output|gradio)-(html|markdown)/.test(node.className)) {
+ translateEl(node, { rich: true });
+ } else if (node.nodeType === 3) {
+ doTranslate(node, node.textContent, "text");
+ } else {
+ translateEl(node, { deep: true });
+ }
+ });
+ }
+ }
+ if (_nodesCount > 0) {
+ const logger = createLogger();
+ logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations);
+ }
+ if (loaded)
+ return;
+ const i18n2 = getI18n();
+ if (i18n2)
+ return;
+ loaded = true;
+ setup3();
+ });
+ observer.observe(gradioApp(), {
+ characterData: true,
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["title", "placeholder"]
+ });
+}
+var customCSS = `
.bilingual__trans_wrapper {
display: inline-flex;
flex-direction: column;
@@ -91,4 +546,10 @@ var F={bilingual_localization_enabled:!0,bilingual_localization_logger:!1,biling
.posex_bg {
white-space: nowrap;
}
- `;document.addEventListener("DOMContentLoaded",()=>{y()});
+ `;
+
+// src/main.ts
+var i18nRegex2 = new Map;
+document.addEventListener("DOMContentLoaded", () => {
+ init();
+});
From 5330e89eb44b7f4e6c2f001ef47e0a6666c3e085 Mon Sep 17 00:00:00 2001
From: Katsuyuki-Karasawa <4ranci0ne@gmail.com>
Date: Sun, 4 Aug 2024 08:29:50 +0900
Subject: [PATCH 5/5] merge issue
---
README.md | 2 +-
biome.json | 4 +--
javascript/bilingual_localization.js | 35 +++++++++++++++---------
scripts/bilingual_localization_helper.py | 4 +++
src/config/opts.ts | 10 +++----
src/init.ts | 3 +-
src/lib/do-translate.ts | 28 +++++++++++++++----
src/lib/translate-el.ts | 9 ++++--
src/main.ts | 30 +-------------------
src/types/opts.d.ts | 1 +
10 files changed, 67 insertions(+), 59 deletions(-)
diff --git a/README.md b/README.md
index 3a0b805..983655b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
```bash
# format & lint
-bunx @biomejs/biome check --config-path=./biome.json
+bunx @biomejs/biome check --write --unsafe --config-path=./biome.json
# build
bun build ./src/main.ts --outfile ./javascript/bilingual_localization.js
diff --git a/biome.json b/biome.json
index 202c4e3..c70281a 100644
--- a/biome.json
+++ b/biome.json
@@ -11,13 +11,13 @@
"formatter": {
"indentStyle": "space",
"lineEnding": "crlf",
- "ignore": ["./javascript", "./scripts"]
+ "ignore": ["javascript", "scripts"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
- "ignore": ["./javascript", "./scripts"]
+ "ignore": ["javascript", "scripts"]
}
}
diff --git a/javascript/bilingual_localization.js b/javascript/bilingual_localization.js
index 4ff5315..f9699c3 100644
--- a/javascript/bilingual_localization.js
+++ b/javascript/bilingual_localization.js
@@ -148,7 +148,7 @@ function translateEl(el, { deep = false, rich = false } = {}) {
if (el.title) {
doTranslate(el, el.title, "title");
}
- if (el.placeholder) {
+ if (el.placeholder && getConfig()?.enableTransPlaceHolder === true) {
doTranslate(el, el.placeholder, "placeholder");
}
if (el.tagName === "OPTION") {
@@ -179,7 +179,12 @@ var ignore_selector = [
"#setting_sd_vae select",
"#txt2img_styles, #img2txt_styles",
".extra-network-cards .card .actions .name",
- "script, style, svg, g, path"
+ "script, style, svg, g, path",
+ "svg *, canvas, canvas *",
+ "#txt2img_prompt_container, #img2img_prompt_container, .physton-prompt",
+ "#txt2img_prompt_container *, #img2img_prompt_container *, .physton-prompt *",
+ ".progressDiv, .progress, .progress-text",
+ ".progressDiv *, .progress *, .progress-text *"
];
// src/lib/tranlate-page.ts
@@ -347,13 +352,21 @@ function doTranslate(el, source, type) {
if (config2?.order === "Original First") {
[trimmedSource, translation] = [translation, trimmedSource];
}
+ const isTranslationIncludeSource = translation.startsWith(source);
switch (type) {
case "text":
el.textContent = translation;
break;
case "element": {
- const htmlStr = `${htmlEncode(translation)}${htmlEncode(trimmedSource)}
`;
- const htmlEl = parseHtmlStringToElement(htmlStr);
+ if (isTranslationIncludeSource) {
+ if (el.nodeType === 3) {
+ el.nodeValue = translation;
+ } else if (htmlEncode(el.textContent) === el.innerHTML) {
+ el.innerHTML = htmlEncode(translation);
+ }
+ break;
+ }
+ const htmlEl = parseHtmlStringToElement(`${htmlEncode(translation)}${htmlEncode(source)}
`);
if (el.hasChildNodes()) {
const textNode = Array.from(el.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() === trimmedSource || node.textContent?.trim() === "__bilingual__will_be_replaced__");
if (textNode) {
@@ -377,13 +390,13 @@ function doTranslate(el, source, type) {
break;
}
case "option":
- el.textContent = `${translation} (${trimmedSource})`;
+ el.textContent = isTranslationIncludeSource ? translation : `${translation} (${trimmedSource})`;
break;
case "title":
- el.title = `${translation}\n${trimmedSource}`;
+ el.title = isTranslationIncludeSource ? translation : `${translation}\n${trimmedSource}`;
break;
case "placeholder":
- el.placeholder = `${translation}\n\n${trimmedSource}`;
+ el.placeholder = isTranslationIncludeSource ? translation : `${translation}\n\n${trimmedSource}`;
break;
default:
return translation;
@@ -458,7 +471,7 @@ var customCSS = `
display: inline-flex;
flex-direction: column;
align-items: center;
- font-size: 13px;
+ font-size: var(--section-header-text-size);
line-height: 1;
}
@@ -494,7 +507,6 @@ var customCSS = `
.posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
#dynamic-prompting .bilingual__trans_wrapper
{
- font-size: 12px;
align-items: flex-start;
}
@@ -549,7 +561,4 @@ var customCSS = `
`;
// src/main.ts
-var i18nRegex2 = new Map;
-document.addEventListener("DOMContentLoaded", () => {
- init();
-});
+document.addEventListener("DOMContentLoaded", init);
diff --git a/scripts/bilingual_localization_helper.py b/scripts/bilingual_localization_helper.py
index 3a7c570..b9051a6 100644
--- a/scripts/bilingual_localization_helper.py
+++ b/scripts/bilingual_localization_helper.py
@@ -53,7 +53,11 @@ def on_ui_settings():
# translation order
shared.opts.add_option("bilingual_localization_order", shared.OptionInfo("Translation First", "Translation display order", gr.Radio, {"choices": ["Translation First", "Original First"]}, section=BL_SECTION))
+ # translate the inputbox's placeholder
+ shared.opts.add_option("bilingual_translate_placeholder", shared.OptionInfo(False, "Enable translate the inputbox's placeholder", section=BL_SECTION))
+
# all localization files path in hidden option
shared.opts.add_option("bilingual_localization_dirs", shared.OptionInfo(json.dumps(I18N_DIRS), "Localization dirs", section=BL_SECTION, component_args={"visible": False}))
+ shared.opts.data["bilingual_localization_dirs"] = json.dumps(I18N_DIRS)
script_callbacks.on_ui_settings(on_ui_settings)
diff --git a/src/config/opts.ts b/src/config/opts.ts
index e14a959..b3502fe 100644
--- a/src/config/opts.ts
+++ b/src/config/opts.ts
@@ -1,9 +1,9 @@
import type { Opts } from "../types/opts";
export const opts: Opts = {
- bilingual_localization_enabled: true, // 初期値を設定
- bilingual_localization_logger: false, // 初期値を設定
- bilingual_localization_file: "None", // 初期値を設定
- bilingual_localization_dirs: "{}", // 初期値を設定
- bilingual_localization_order: "Translation First", // 初期値を設定
+ bilingual_localization_enabled: true,
+ bilingual_localization_logger: false,
+ bilingual_localization_file: "None",
+ bilingual_localization_dirs: "{}",
+ bilingual_localization_order: "Translation First",
};
diff --git a/src/init.ts b/src/init.ts
index 77a4323..954b70e 100644
--- a/src/init.ts
+++ b/src/init.ts
@@ -10,7 +10,7 @@ const customCSS = `
display: inline-flex;
flex-direction: column;
align-items: center;
- font-size: 13px;
+ font-size: var(--section-header-text-size);
line-height: 1;
}
@@ -46,7 +46,6 @@ const customCSS = `
.posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
#dynamic-prompting .bilingual__trans_wrapper
{
- font-size: 12px;
align-items: flex-start;
}
diff --git a/src/lib/do-translate.ts b/src/lib/do-translate.ts
index 3082b9c..098bd56 100644
--- a/src/lib/do-translate.ts
+++ b/src/lib/do-translate.ts
@@ -42,14 +42,26 @@ export function doTranslate(el, source, type) {
[trimmedSource, translation] = [translation, trimmedSource];
}
+ const isTranslationIncludeSource = translation.startsWith(source);
+
switch (type) {
case "text":
el.textContent = translation;
break;
case "element": {
- const htmlStr = `${htmlEncode(translation)}${htmlEncode(trimmedSource)}
`;
- const htmlEl = parseHtmlStringToElement(htmlStr);
+ if (isTranslationIncludeSource) {
+ if (el.nodeType === 3) {
+ el.nodeValue = translation;
+ } else if (htmlEncode(el.textContent) === el.innerHTML) {
+ el.innerHTML = htmlEncode(translation);
+ }
+ break;
+ }
+
+ const htmlEl = parseHtmlStringToElement(
+ `${htmlEncode(translation)}${htmlEncode(source)}
`,
+ );
if (el.hasChildNodes()) {
const textNode = Array.from(el.childNodes).find(
@@ -92,15 +104,21 @@ export function doTranslate(el, source, type) {
}
case "option":
- el.textContent = `${translation} (${trimmedSource})`;
+ el.textContent = isTranslationIncludeSource
+ ? translation
+ : `${translation} (${trimmedSource})`;
break;
case "title":
- el.title = `${translation}\n${trimmedSource}`;
+ el.title = isTranslationIncludeSource
+ ? translation
+ : `${translation}\n${trimmedSource}`;
break;
case "placeholder":
- el.placeholder = `${translation}\n\n${trimmedSource}`;
+ el.placeholder = isTranslationIncludeSource
+ ? translation
+ : `${translation}\n\n${trimmedSource}`;
break;
default:
diff --git a/src/lib/translate-el.ts b/src/lib/translate-el.ts
index 77423d9..569f589 100644
--- a/src/lib/translate-el.ts
+++ b/src/lib/translate-el.ts
@@ -1,4 +1,4 @@
-import { getI18n } from "../setup";
+import { getConfig, getI18n } from "../setup";
import { doTranslate } from "./do-translate";
const ignore_selector = [
@@ -9,6 +9,11 @@ const ignore_selector = [
"#txt2img_styles, #img2txt_styles", // styles select
".extra-network-cards .card .actions .name", // extra network cards name
"script, style, svg, g, path", // script / style / svg elements
+ "svg *, canvas, canvas *",
+ "#txt2img_prompt_container, #img2img_prompt_container, .physton-prompt",
+ "#txt2img_prompt_container *, #img2img_prompt_container *, .physton-prompt *",
+ ".progressDiv, .progress, .progress-text",
+ ".progressDiv *, .progress *, .progress-text *",
];
export function translateEl(el, { deep = false, rich = false } = {}) {
@@ -19,7 +24,7 @@ export function translateEl(el, { deep = false, rich = false } = {}) {
doTranslate(el, el.title, "title");
}
- if (el.placeholder) {
+ if (el.placeholder && getConfig()?.enableTransPlaceHolder === true) {
doTranslate(el, el.placeholder, "placeholder");
}
diff --git a/src/main.ts b/src/main.ts
index 8765f9d..4d8afbd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,31 +1,3 @@
import { init } from "./init";
-interface I18n {
- [key: string]: string;
-}
-interface I18nScope {
- [scope: string]: I18n;
-}
-
-interface ScopedSource {
- [source: string]: string[];
-}
-
-interface Config {
- enabled: boolean;
- file: string;
- dirs: string[];
- order: string;
- enableLogger: boolean;
-}
-
-const i18n: I18n | null = null;
-const i18nRegex: Map = new Map();
-const i18nScope: I18nScope = {};
-const scopedSource: ScopedSource = {};
-const config: Config | null = null;
-
-// DOMContentLoaded イベント発生後に初期化処理を実行
-document.addEventListener("DOMContentLoaded", () => {
- init();
-});
+document.addEventListener("DOMContentLoaded", init);
diff --git a/src/types/opts.d.ts b/src/types/opts.d.ts
index 164aca8..b9ad0e1 100644
--- a/src/types/opts.d.ts
+++ b/src/types/opts.d.ts
@@ -1,4 +1,5 @@
export interface Opts {
+ bilingual_translate_placeholder: boolean;
bilingual_localization_enabled: boolean;
bilingual_localization_logger: boolean;
bilingual_localization_file: string;