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

# sd-webui-bilingual-localization @@ -7,6 +5,13 @@ ![Snipaste_2023-03-30_01-05-45](https://user-images.githubusercontent.com/16256221/228617304-3107244b-ce13-4b96-b665-1d13090d24a7.png) +## 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

- -# sd-webui-bilingual-localization -[Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)のバイリンガル対応拡張機能 - -![Snipaste_2023-03-30_01-05-45](https://user-images.githubusercontent.com/16256221/228617304-3107244b-ce13-4b96-b665-1d13090d24a7.png) - -## 特徴 -- バイリンガル対応により、元のボタンを探す必要がありません。 -- 日本語化拡張機能と互換性があり、ファイルを取り込み直す必要はありません。 -- ツールチップの動的翻訳をサポートします。 -- スコープと正規表現パターンによる柔軟な翻訳が可能です。 - -## インストール - -以下の方法から選択します。 -拡張機能に対応したWebUI(2023年以降のバージョン)が必要です。 - -#### 方法1 - -WebUIの`Install from URL`でインストールを行います。 - -Extensions - Install from URLを順にクリックします。 - -1個目のテキストボックスに`https://github.com/journey-ad/sd-webui-bilingual-localization`を入力し、Installボタンをクリックします。 - -![Snipaste_2023-02-28_00-27-48](https://user-images.githubusercontent.com/16256221/221625310-a6ef0b4c-a1e0-46bb-be9c-6d88cd0ad684.png) - -その後、Installedパネルに切り替え、Apply and restart UIボタンをクリックします。 - -![Snipaste_2023-02-28_00-29-14](https://user-images.githubusercontent.com/16256221/221625345-9e656f25-89dd-4361-8ee5-f4ab39d18ca4.png) - - -#### 方法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ボタンを順にクリックします。 - -![Snipaste_2023-02-28_00-04-21](https://user-images.githubusercontent.com/16256221/221625729-73519629-8c1f-4eb5-99db-a1d3f4b58a87.png) - -## スコープ - -ローカリゼーションは、グローバルな影響を防ぐためにスコープを限定したサポートを提供します。構文規則は以下の通りです: -- `####` スコープが指定された要素の祖先の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

- -# sd-webui-bilingual-localization -[Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 双语对照翻译插件 - -![Snipaste_2023-03-30_01-05-45](https://user-images.githubusercontent.com/16256221/228617304-3107244b-ce13-4b96-b665-1d13090d24a7.png) - -## 功能 -- 全新实现的双语对照翻译功能,不必再担心切换翻译后找不到原始功能 -- 兼容原生语言包扩展,无需重新导入多语言语料 -- 支持动态title提示的翻译 -- 额外支持作用域和正则表达式替换,翻译更加灵活 - -## 安装 - -以下方式选择其一,需要使用支持扩展功能的 webui (2023年之后的版本) - -#### 方式1 - -使用 webui 提供的`Install from URL`功能安装 - -按下图所示,依次点击Extensions - Install from URL - -然后在第一个文本框内填入`https://github.com/journey-ad/sd-webui-bilingual-localization`,点击Install按钮 -![Snipaste_2023-02-28_00-27-48](https://user-images.githubusercontent.com/16256221/221625310-a6ef0b4c-a1e0-46bb-be9c-6d88cd0ad684.png) - -之后切换到Installed面板,点击Apply and restart UI按钮 -![Snipaste_2023-02-28_00-29-14](https://user-images.githubusercontent.com/16256221/221625345-9e656f25-89dd-4361-8ee5-f4ab39d18ca4.png) - - -#### 方式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 settingsReload UI按钮 -![Snipaste_2023-02-28_00-04-21](https://user-images.githubusercontent.com/16256221/221625729-73519629-8c1f-4eb5-99db-a1d3f4b58a87.png) - -## 作用域支持 - -本地化语料支持限定作用域,防止影响全局翻译,语法规则: -- `####` 仅当节点祖先元素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 @@ ![Snipaste_2023-03-30_01-05-45](https://user-images.githubusercontent.com/16256221/228617304-3107244b-ce13-4b96-b665-1d13090d24a7.png) -## 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;