├── .gitignore
├── LICENSE
├── scripts
└── bilingual_localization_helper.py
├── README_ZH.md
├── README_JA.md
├── README.md
└── javascript
└── bilingual_localization.js
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Jad
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scripts/bilingual_localization_helper.py:
--------------------------------------------------------------------------------
1 | # This helper script loads the list of localization files and
2 | # exposes the current localization file name and path to the javascript side
3 |
4 | import os
5 | import gradio as gr
6 | from pathlib import Path
7 | from modules import script_callbacks, shared
8 | import json
9 |
10 | localizations = {}
11 | localizations_dir = shared.cmd_opts.localizations_dir if "localizations_dir" in shared.cmd_opts else "localizations"
12 |
13 | def list_localizations(dirname):
14 | localizations.clear()
15 |
16 | print("dirname: ", dirname)
17 |
18 | for file in os.listdir(dirname):
19 | fn, ext = os.path.splitext(file)
20 | if ext.lower() != ".json":
21 | continue
22 |
23 | localizations[fn] = os.path.join(dirname, file)
24 |
25 | from modules import scripts
26 | for file in scripts.list_scripts("localizations", ".json"):
27 | fn, ext = os.path.splitext(file.filename)
28 | localizations[fn] = file.path
29 |
30 | print("localizations: ", localizations)
31 |
32 |
33 | list_localizations(localizations_dir)
34 |
35 | # Webui root path
36 | ROOT_DIR = Path().absolute()
37 |
38 | # The localization files
39 | I18N_DIRS = { k: str(Path(v).relative_to(ROOT_DIR).as_posix()) for k, v in localizations.items() }
40 |
41 | # Register extension options
42 | def on_ui_settings():
43 | BL_SECTION = ("bl", "Bilingual Localization")
44 | # enable in settings
45 | shared.opts.add_option("bilingual_localization_enabled", shared.OptionInfo(True, "Enable Bilingual Localization", section=BL_SECTION))
46 |
47 | # enable devtools log
48 | shared.opts.add_option("bilingual_localization_logger", shared.OptionInfo(False, "Enable Devtools Log", section=BL_SECTION))
49 |
50 | # current localization file
51 | shared.opts.add_option("bilingual_localization_file", shared.OptionInfo("None", "Localization file (Please leave `User interface` - `Localization` as None)", gr.Dropdown, lambda: {"choices": ["None"] + list(localizations.keys())}, refresh=lambda: list_localizations(localizations_dir), section=BL_SECTION))
52 |
53 | # translation order
54 | shared.opts.add_option("bilingual_localization_order", shared.OptionInfo("Translation First", "Translation display order", gr.Radio, {"choices": ["Translation First", "Original First"]}, section=BL_SECTION))
55 |
56 | # all localization files path in hidden option
57 | shared.opts.add_option("bilingual_localization_dirs", shared.OptionInfo(json.dumps(I18N_DIRS), "Localization dirs", section=BL_SECTION, component_args={"visible": False}))
58 |
59 | script_callbacks.on_ui_settings(on_ui_settings)
60 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 | [English Version](README.md)
2 |
3 |

4 |
5 | # sd-webui-bilingual-localization
6 | [Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 双语对照翻译插件
7 |
8 | 
9 |
10 | ## 功能
11 | - 全新实现的双语对照翻译功能,不必再担心切换翻译后找不到原始功能
12 | - 兼容原生语言包扩展,无需重新导入多语言语料
13 | - 支持动态title提示的翻译
14 | - 额外支持作用域和正则表达式替换,翻译更加灵活
15 |
16 | ## 安装
17 |
18 | 以下方式选择其一,需要使用支持扩展功能的 webui (2023年之后的版本)
19 |
20 | #### 方式1
21 |
22 | 使用 webui 提供的`Install from URL`功能安装
23 |
24 | 按下图所示,依次点击Extensions - Install from URL
25 |
26 | 然后在第一个文本框内填入`https://github.com/journey-ad/sd-webui-bilingual-localization`,点击Install按钮
27 | 
28 |
29 | 之后切换到Installed面板,点击Apply and restart UI按钮
30 | 
31 |
32 |
33 | #### 方式2
34 |
35 | 手动克隆到你的扩展目录里
36 |
37 | ```bash
38 | git clone https://github.com/journey-ad/sd-webui-bilingual-localization extensions/sd-webui-bilingual-localization
39 | ```
40 |
41 | ## 使用
42 |
43 | > **⚠️重要⚠️**
44 | > 确保Settings - User interface - Localization 已设置为了 `None`
45 |
46 | 在Settings - Bilingual Localization中选择要启用的本地化文件,依次点击Apply settings和Reload UI按钮
47 | 
48 |
49 | ## 作用域支持
50 |
51 | 本地化语料支持限定作用域,防止影响全局翻译,语法规则:
52 | - `####` 仅当节点祖先元素ID匹配指定的作用域时才会生效
53 | - `##@##` 仅当节点祖先元素匹配指定的CSS选择器时才会生效
54 |
55 | ```json
56 | {
57 | ...
58 | "##tab_ti##Normal": "正态", // 仅id="tab_ti"元素下的`Normal`会被翻译为`正态`
59 | "##tab_threedopenpose##Normal": "法线图", // 仅id="tab_threedopenpose"元素下的`Normal`会被翻译为`法线图`
60 | "##@.extra-networks .tab-nav button##Lora": "Lora模型", // 仅class=".extra-networks .tab-nav button"元素下的`Lora`会被翻译为`Lora模型`
61 | ...
62 | }
63 | ```
64 |
65 | ## 正则表达式支持
66 |
67 | 本地化语料支持正则表达式替换,语法规则`@@`,括号匹配变量`$n`,参考[String.prototype.replace()](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
68 | ```json
69 | {
70 | ...
71 | "@@/^(\\d+) images in this directory, divided into (\\d+) pages$/": "目录中有$1张图片,共$2页",
72 | "@@/^Favorites path from settings: (.*)$/": "设置的收藏夹目录:$1",
73 | ...
74 | }
75 | ```
76 |
77 | ## 获取本地化文件
78 |
79 | 本地化文件不再随插件提供,请安装第三方语言包并按照本文[使用](#使用)部分的方式设置使用
80 |
81 | *预览图片中的语言包可以在这里找到 https://gist.github.com/journey-ad/d98ed173321658be6e51f752d6e6163c*
82 |
--------------------------------------------------------------------------------
/README_JA.md:
--------------------------------------------------------------------------------
1 | [English Version](README.md)
2 |
3 | 
4 |
5 | # sd-webui-bilingual-localization
6 | [Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)のバイリンガル対応拡張機能
7 |
8 | 
9 |
10 | ## 特徴
11 | - バイリンガル対応により、元のボタンを探す必要がありません。
12 | - 日本語化拡張機能と互換性があり、ファイルを取り込み直す必要はありません。
13 | - ツールチップの動的翻訳をサポートします。
14 | - スコープと正規表現パターンによる柔軟な翻訳が可能です。
15 |
16 | ## インストール
17 |
18 | 以下の方法から選択します。
19 | 拡張機能に対応したWebUI(2023年以降のバージョン)が必要です。
20 |
21 | #### 方法1
22 |
23 | WebUIの`Install from URL`でインストールを行います。
24 |
25 | Extensions - Install from URLを順にクリックします。
26 |
27 | 1個目のテキストボックスに`https://github.com/journey-ad/sd-webui-bilingual-localization`を入力し、Installボタンをクリックします。
28 |
29 | 
30 |
31 | その後、Installedパネルに切り替え、Apply and restart UIボタンをクリックします。
32 |
33 | 
34 |
35 |
36 | #### 方法2
37 |
38 | 拡張機能のディレクトリに手動でcloneします。
39 |
40 | ```bash
41 | git clone https://github.com/journey-ad/sd-webui-bilingual-localization extensions/sd-webui-bilingual-localization
42 | ```
43 |
44 | ## 使用方法
45 |
46 | > **⚠️重要⚠️**
47 | > Settings - User interface - Localizationが`None`に設定されていることを確認してください。
48 |
49 | Settings - Bilingual Localizationパネルで、有効にしたい言語ファイル名を選択し、Apply settingsボタンとReload UIボタンを順にクリックします。
50 |
51 | 
52 |
53 | ## スコープ
54 |
55 | ローカリゼーションは、グローバルな影響を防ぐためにスコープを限定したサポートを提供します。構文規則は以下の通りです:
56 | - `####` スコープが指定された要素の祖先のIDと一致する場合にのみ、スコープ付きのテキストが適用されます。
57 | - `##@##` スコープが指定されたCSSセレクタと一致する場合にのみ、スコープ付きのテキストが適用されます。
58 |
59 | ```json
60 | ...
61 | "##tab_ti##Normal": "正常", // id="tab_ti"の要素の下にある`Normal`のみが`正常`に変換されます
62 | "##tab_threedopenpose##Normal": "法線マップ", // id="tab_threedopenpose"の要素の下にある`Normal`のみが `法線マップ`に変換されます
63 | "##@.extra-networks .tab-nav button##Lora": "Loraモデル", // class=".extra-networks .tab-nav button"の要素の下にある`Lora`のみが`Loraモデル`に変換されます
64 | ...
65 | ```
66 |
67 | ## 正規表現パターン
68 |
69 | 正規表現を使った日本語化が可能です。構文ルールは`@@`、キャプチャグループは`$n`です。ドキュメント:[String.prototype.replace()](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replace)。
70 | ```json
71 | {
72 | ...
73 | "@@/^(\\d+) images in this directory, divided into (\\d+) pages$/": "このディレクトリには$1枚の画像、$2ページ",
74 | "@@/^Favorites path from settings: (.*)$/": "お気に入りのディレクトリパス:$1",
75 | ...
76 | }
77 | ```
78 |
79 | ## 日本語化ファイルの取得
80 |
81 | 内蔵の日本語化ファイルは提供されなくなりました。サードパーティーの日本語化拡張機能をインストールし、当ページの[使用方法](#使用方法)に記載されている方法でセットアップしてください。
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [中文文档](README_ZH.md) / [日本語](README_JA.md)
2 |
3 | 
4 |
5 | # sd-webui-bilingual-localization
6 | [Stable Diffusion web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) bilingual localization extensions.
7 |
8 | 
9 |
10 | ## Features
11 | - Bilingual translation, no need to worry about how to find the original button.
12 | - Compatible with language pack extensions, no need to re-import.
13 | - Support dynamic translation of title hints.
14 | - Additional support Scoped and RegExp pattern, more flexible translation.
15 |
16 | ## Installation
17 |
18 | Choose one of the following methods, Need to use webui with extension support (Versions after 2023)
19 |
20 | #### Method 1
21 |
22 | Use the `Install from URL` provided by webui to install
23 |
24 | Click in order Extensions - Install from URL
25 |
26 | Then fill in the first text box with `https://github.com/journey-ad/sd-webui-bilingual-localization`, click the Install button.
27 |
28 | 
29 |
30 | After that, switch to the Installed panel and click the Apply and restart UI button.
31 |
32 | 
33 |
34 |
35 | #### Method 2
36 |
37 | Clone to your extension directory manually.
38 |
39 | ```bash
40 | git clone https://github.com/journey-ad/sd-webui-bilingual-localization extensions/sd-webui-bilingual-localization
41 | ```
42 |
43 | ## Usage
44 |
45 | > **⚠️Important⚠️**
46 | > Make sure Settings - User interface - Localization is set to `None`
47 |
48 | In Settings - Bilingual Localization panel, select the localization file you want to enable and click on the Apply settings and Reload UI buttons in turn.
49 |
50 | 
51 |
52 | ## Scoped
53 |
54 | Localization supports scoped to prevent global polluting. The syntax rules are as follows:
55 | - `####` Scoped text will only take effect when the ancestor element ID matches the specified scope.
56 | - `##@##` Scoped text will only take effect when the ancestor element matches the specified CSS selector.
57 |
58 | ```json
59 | {
60 | ...
61 | "##tab_ti##Normal": "正态", // only the text `Normal` under the element with id="tab_ti" will be translated to `正态`.
62 | "##tab_threedopenpose##Normal": "法线图", // only the text `Normal` under the element with id="tab_threedopenpose" will be translated to `法线图`.
63 | "##@.extra-networks .tab-nav button##Lora": "Lora模型", // only the text `Lora` under the element with class=".extra-networks .tab-nav button" will be translated to `Lora模型`.
64 | ...
65 | }
66 | ```
67 |
68 | ## RegExp pattern
69 |
70 | Localization support RegExp pattern, syntax rule is `@@`, capturing group is `$n`, doc: [String.prototype.replace()](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
71 | ```json
72 | {
73 | ...
74 | "@@/^(\\d+) images in this directory, divided into (\\d+) pages$/": "目录中有$1张图片,共$2页",
75 | "@@/^Favorites path from settings: (.*)$/": "设置的收藏夹目录:$1",
76 | ...
77 | }
78 | ```
79 |
80 | ## How to get localization file
81 |
82 | Localization files are no longer provided with the plugin, please install a third-party language extensions and set-up as described in the [Usage](#usage) section of this article.
83 |
--------------------------------------------------------------------------------
/javascript/bilingual_localization.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const customCSS = `
3 | .bilingual__trans_wrapper {
4 | display: inline-flex;
5 | flex-direction: column;
6 | align-items: center;
7 | font-size: 13px;
8 | line-height: 1;
9 | }
10 |
11 | .bilingual__trans_wrapper em {
12 | font-style: normal;
13 | }
14 |
15 | #txtimg_hr_finalres .bilingual__trans_wrapper em,
16 | #tab_ti .output-html .bilingual__trans_wrapper em,
17 | #tab_ti .gradio-html .bilingual__trans_wrapper em,
18 | #sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em,
19 | #available_extensions .extension-tag .bilingual__trans_wrapper em,
20 | #available_extensions .date_added .bilingual__trans_wrapper em,
21 | #available_extensions+p>.bilingual__trans_wrapper em,
22 | .gradio-image div[data-testid="image"] .bilingual__trans_wrapper em {
23 | display: none;
24 | }
25 |
26 | #settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
27 | label>span>.bilingual__trans_wrapper,
28 | fieldset>span>.bilingual__trans_wrapper,
29 | .label-wrap>span>.bilingual__trans_wrapper,
30 | .w-full>span>.bilingual__trans_wrapper,
31 | .context-menu-items .bilingual__trans_wrapper,
32 | .single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper,
33 | .output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper),
34 | .gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper),
35 | .output-markdown .bilingual__trans_wrapper,
36 | .gradio-markdown .bilingual__trans_wrapper,
37 | .gradio-image>div.float .bilingual__trans_wrapper,
38 | .gradio-file>div.float .bilingual__trans_wrapper,
39 | .gradio-code>div.float .bilingual__trans_wrapper,
40 | .posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */
41 | #dynamic-prompting .bilingual__trans_wrapper
42 | {
43 | font-size: 12px;
44 | align-items: flex-start;
45 | }
46 |
47 | #extensions label .bilingual__trans_wrapper,
48 | #available_extensions td .bilingual__trans_wrapper,
49 | .label-wrap>span>.bilingual__trans_wrapper {
50 | font-size: inherit;
51 | line-height: inherit;
52 | }
53 |
54 | .label-wrap>span:first-of-type {
55 | font-size: 13px;
56 | line-height: 1;
57 | }
58 |
59 | #txt2img_script_container > div {
60 | margin-top: var(--layout-gap, 12px);
61 | }
62 |
63 | textarea::placeholder {
64 | line-height: 1;
65 | padding: 4px 0;
66 | }
67 |
68 | label>span {
69 | line-height: 1;
70 | }
71 |
72 | div[data-testid="image"] .start-prompt {
73 | background-color: rgba(255, 255, 255, .6);
74 | color: #222;
75 | transition: opacity .2s ease-in-out;
76 | }
77 | div[data-testid="image"]:hover .start-prompt {
78 | opacity: 0;
79 | }
80 |
81 | .label-wrap > span.icon {
82 | width: 1em;
83 | height: 1em;
84 | transform-origin: center center;
85 | }
86 |
87 | .gradio-dropdown ul.options li.item {
88 | padding: 0.3em 0.4em !important;
89 | }
90 |
91 | /* Posex extension */
92 | .posex_bg {
93 | white-space: nowrap;
94 | }
95 | `
96 |
97 | let i18n = null, i18nRegex = new Map(), i18nScope = {}, scopedSource = {}, config = null;
98 |
99 | // First load
100 | function setup() {
101 | config = {
102 | enabled: opts["bilingual_localization_enabled"],
103 | file: opts["bilingual_localization_file"],
104 | dirs: opts["bilingual_localization_dirs"],
105 | order: opts["bilingual_localization_order"],
106 | enableLogger: opts["bilingual_localization_logger"]
107 | }
108 |
109 | let { enabled, file, dirs, enableLogger } = config
110 |
111 | if (!enabled || file === "None" || dirs === "None") return
112 |
113 | dirs = JSON.parse(dirs)
114 |
115 | enableLogger && logger.init('Bilingual')
116 | logger.log('Bilingual Localization initialized.')
117 |
118 | // Load localization file
119 | const regex_scope = /^##(?.+)##(?.+)$/ // ##scope##.skey
120 | i18n = JSON.parse(readFile(dirs[file]), (key, value) => {
121 | // parse regex translations
122 | if (key.startsWith('@@')) {
123 | const regex = getRegex(key.slice(2))
124 | if (regex instanceof RegExp) {
125 | i18nRegex.set(regex, value)
126 | }
127 | } else if (regex_scope.test(key)) {
128 | // parse scope translations
129 | let { scope, skey } = key.match(regex_scope).groups
130 |
131 | if (scope.startsWith('@')) {
132 | scope = scope.slice(1)
133 | } else {
134 | scope = '#' + scope
135 | }
136 |
137 | if (!scope.length) {
138 | return value
139 | }
140 |
141 | i18nScope[scope] ||= {}
142 | i18nScope[scope][skey] = value
143 |
144 | scopedSource[skey] ||= []
145 | scopedSource[skey].push(scope)
146 | } else {
147 | return value
148 | }
149 | })
150 |
151 | logger.group('Localization file loaded.')
152 | logger.log('i18n', i18n)
153 | logger.log('i18nRegex', i18nRegex)
154 | logger.log('i18nScope', i18nScope)
155 | logger.groupEnd()
156 |
157 | translatePage()
158 | handleDropdown()
159 | }
160 |
161 | function handleDropdown() {
162 | // process gradio dropdown menu
163 | delegateEvent(gradioApp(), 'mousedown', 'ul.options .item', function (event) {
164 | const { target } = event
165 |
166 | if (!target.classList.contains('item')) {
167 | // simulate click menu item
168 | target.closest('.item').dispatchEvent(new Event('mousedown', { bubbles: true }))
169 | return
170 | }
171 |
172 | const source = target.dataset.value
173 | const $labelEl = target?.closest('.wrap')?.querySelector('.wrap-inner .single-select') // the label element
174 |
175 | if (source && $labelEl) {
176 | $labelEl.title = titles?.[source] || '' // set title from hints.js
177 | $labelEl.textContent = "__biligual__will_be_replaced__" // marked as will be replaced
178 | doTranslate($labelEl, source, 'element') // translate the label element
179 | }
180 | });
181 | }
182 |
183 | // Translate page
184 | function translatePage() {
185 | if (!i18n) return
186 |
187 | logger.time('Full Page')
188 | querySelectorAll([
189 | "label span, fieldset span, button", // major label and button description text
190 | "textarea[placeholder], select, option", // text box placeholder and select element
191 | ".transition > div > span:not([class])", ".label-wrap > span", // collapse panel added by extension
192 | ".gradio-image>div.float", // image upload description
193 | ".gradio-file>div.float", // file upload description
194 | ".gradio-code>div.float", // code editor description
195 | "#modelmerger_interp_description .output-html", // model merger description
196 | "#modelmerger_interp_description .gradio-html", // model merger description
197 | "#lightboxModal span" // image preview lightbox
198 | ])
199 | .forEach(el => translateEl(el, { deep: true }))
200 |
201 | querySelectorAll([
202 | 'div[data-testid="image"] > div > div', // description of image upload panel
203 | '#extras_image_batch > div', // description of extras image batch file upload panel
204 | ".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", // output html exclude footer
205 | '#dynamic-prompting' // dynamic-prompting extension
206 | ])
207 | .forEach(el => translateEl(el, { rich: true }))
208 |
209 | logger.timeEnd('Full Page')
210 | }
211 |
212 | const ignore_selector = [
213 | '.bilingual__trans_wrapper', // self
214 | '.resultsFlexContainer', // tag-autocomplete
215 | '#setting_sd_model_checkpoint select', // model checkpoint
216 | '#setting_sd_vae select', // vae model
217 | '#txt2img_styles, #img2txt_styles', // styles select
218 | '.extra-network-cards .card .actions .name', // extra network cards name
219 | 'script, style, svg, g, path', // script / style / svg elements
220 | ]
221 | // Translate element
222 | function translateEl(el, { deep = false, rich = false } = {}) {
223 | if (!i18n) return // translation not ready.
224 | if (el.matches?.(ignore_selector)) return // ignore some elements.
225 |
226 | if (el.title) {
227 | doTranslate(el, el.title, 'title')
228 | }
229 |
230 | if (el.placeholder) {
231 | doTranslate(el, el.placeholder, 'placeholder')
232 | }
233 |
234 | if (el.tagName === 'OPTION') {
235 | doTranslate(el, el.textContent, 'option')
236 | }
237 |
238 | if (deep || rich) {
239 | Array.from(el.childNodes).forEach(node => {
240 | if (node.nodeName === '#text') {
241 | if (rich) {
242 | doTranslate(node, node.textContent, 'text')
243 | return
244 | }
245 |
246 | if (deep) {
247 | doTranslate(node, node.textContent, 'element')
248 | }
249 | } else if (node.childNodes.length > 0) {
250 | translateEl(node, { deep, rich })
251 | }
252 | })
253 | } else {
254 | doTranslate(el, el.textContent, 'element')
255 | }
256 | }
257 |
258 | function checkRegex(source) {
259 | for (const [regex, value] of i18nRegex.entries()) {
260 | if (regex.test(source)) {
261 | logger.log('regex', regex, source, value)
262 | return source.replace(regex, value)
263 | }
264 | }
265 | }
266 |
267 | const re_num = /^[\.\d]+$/,
268 | re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
269 |
270 | function doTranslate(el, source, type) {
271 | if (!i18n) return // translation not ready.
272 | source = source.trim()
273 | if (!source) return
274 | if (re_num.test(source)) return
275 | // if (re_emoji.test(source)) return
276 |
277 | let translation = i18n[source] || checkRegex(source),
278 | scopes = scopedSource[source]
279 |
280 | if (scopes) {
281 | console.log('scope', el, source, scopes);
282 | for (let scope of scopes) {
283 | if (el.parentElement.closest(scope)) {
284 | translation = i18nScope[scope][source]
285 | break
286 | }
287 | }
288 | }
289 |
290 | if (!translation || source === translation) {
291 | if (el.textContent === '__biligual__will_be_replaced__') el.textContent = source // restore original text if translation not exist
292 | if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() // remove exist translation if translation not exist
293 | return
294 | }
295 |
296 | if (config.order === "Original First") {
297 | [source, translation] = [translation, source]
298 | }
299 |
300 | switch (type) {
301 | case 'text':
302 | el.textContent = translation
303 | break;
304 |
305 | case 'element':
306 | const htmlStr = `${htmlEncode(translation)}${htmlEncode(source)}
`
307 | const htmlEl = parseHtmlStringToElement(htmlStr)
308 | if (el.hasChildNodes()) {
309 | const textNode = Array.from(el.childNodes).find(node =>
310 | node.nodeName === '#text' &&
311 | (node.textContent.trim() === source || node.textContent.trim() === '__biligual__will_be_replaced__')
312 | )
313 |
314 | if (textNode) {
315 | textNode.textContent = ''
316 | if (textNode.nextSibling?.className === 'bilingual__trans_wrapper') textNode.nextSibling.remove()
317 | textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling)
318 | }
319 | } else {
320 | el.textContent = ''
321 | if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove()
322 | el.parentNode.insertBefore(htmlEl, el.nextSibling)
323 | }
324 | break;
325 |
326 | case 'option':
327 | el.textContent = `${translation} (${source})`
328 | break;
329 |
330 | case 'title':
331 | el.title = `${translation}\n${source}`
332 | break;
333 |
334 | case 'placeholder':
335 | el.placeholder = `${translation}\n\n${source}`
336 | break;
337 |
338 | default:
339 | return translation
340 | }
341 | }
342 |
343 | function gradioApp() {
344 | const elems = document.getElementsByTagName('gradio-app')
345 | const elem = elems.length == 0 ? document : elems[0]
346 |
347 | if (elem !== document) elem.getElementById = function (id) { return document.getElementById(id) }
348 | return elem.shadowRoot ? elem.shadowRoot : elem
349 | }
350 |
351 | function querySelector(...args) {
352 | return gradioApp()?.querySelector(...args)
353 | }
354 |
355 | function querySelectorAll(...args) {
356 | return gradioApp()?.querySelectorAll(...args)
357 | }
358 |
359 | function delegateEvent(parent, eventType, selector, handler) {
360 | parent.addEventListener(eventType, function (event) {
361 | var target = event.target;
362 | while (target !== parent) {
363 | if (target.matches(selector)) {
364 | handler.call(target, event);
365 | }
366 | target = target.parentNode;
367 | }
368 | });
369 | }
370 |
371 | function parseHtmlStringToElement(htmlStr) {
372 | const template = document.createElement('template')
373 | template.insertAdjacentHTML('afterbegin', htmlStr)
374 | return template.firstElementChild
375 | }
376 |
377 | function htmlEncode(htmlStr) {
378 | return htmlStr.replace(/&/g, '&').replace(//g, '>')
379 | .replace(/"/g, '"').replace(/'/g, ''')
380 | }
381 |
382 | // get regex object from string
383 | function getRegex(regex) {
384 | try {
385 | regex = regex.trim();
386 | let parts = regex.split('/');
387 | if (regex[0] !== '/' || parts.length < 3) {
388 | regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string
389 | return new RegExp(regex);
390 | }
391 |
392 | const option = parts[parts.length - 1];
393 | const lastIndex = regex.lastIndexOf('/');
394 | regex = regex.substring(1, lastIndex);
395 | return new RegExp(regex, option);
396 | } catch (e) {
397 | return null
398 | }
399 | }
400 |
401 | // Load file
402 | function readFile(filePath) {
403 | let request = new XMLHttpRequest();
404 | request.open("GET", `file=${filePath}`, false);
405 | request.send(null);
406 | return request.responseText;
407 | }
408 |
409 | const logger = (function () {
410 | const loggerTimerMap = new Map()
411 | const loggerConf = { badge: true, label: 'Logger', enable: false }
412 | return new Proxy(console, {
413 | get: (target, propKey) => {
414 | if (propKey === 'init') {
415 | return (label) => {
416 | loggerConf.label = label
417 | loggerConf.enable = true
418 | }
419 | }
420 |
421 | if (!(propKey in target)) return undefined
422 |
423 | return (...args) => {
424 | if (!loggerConf.enable) return
425 |
426 | let color = ['#39cfe1', '#006cab']
427 |
428 | let label, start
429 | switch (propKey) {
430 | case 'error':
431 | color = ['#f70000', '#a70000']
432 | break;
433 | case 'warn':
434 | color = ['#f7b500', '#b58400']
435 | break;
436 | case 'time':
437 | label = args[0]
438 | if (loggerTimerMap.has(label)) {
439 | logger.warn(`Timer '${label}' already exisits`)
440 | } else {
441 | loggerTimerMap.set(label, performance.now())
442 | }
443 | return
444 | case 'timeEnd':
445 | label = args[0], start = loggerTimerMap.get(label)
446 | if (start === undefined) {
447 | logger.warn(`Timer '${label}' does not exist`)
448 | } else {
449 | loggerTimerMap.delete(label)
450 | logger.log(`${label}: ${performance.now() - start} ms`)
451 | }
452 | return
453 | case 'groupEnd':
454 | loggerConf.badge = true
455 | break
456 | }
457 |
458 | 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;`] : []
459 |
460 | target[propKey](...badge, ...args)
461 |
462 | if (propKey === 'group' || propKey === 'groupCollapsed') {
463 | loggerConf.badge = false
464 | }
465 | }
466 | }
467 | })
468 | }())
469 |
470 | function init() {
471 | // Add style to dom
472 | let $styleEL = document.createElement('style');
473 |
474 | if ($styleEL.styleSheet) {
475 | $styleEL.styleSheet.cssText = customCSS;
476 | } else {
477 | $styleEL.appendChild(document.createTextNode(customCSS));
478 | }
479 | gradioApp().appendChild($styleEL);
480 |
481 | let loaded = false
482 | let _count = 0
483 |
484 | const observer = new MutationObserver(mutations => {
485 | if (window.localization && Object.keys(window.localization).length) return; // disabled if original translation enabled
486 | if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
487 |
488 | let _nodesCount = 0, _now = performance.now()
489 |
490 | for (const mutation of mutations) {
491 | if (mutation.type === 'characterData') {
492 | if (mutation.target?.parentElement?.parentElement?.tagName === 'LABEL') {
493 | translateEl(mutation.target)
494 | }
495 | } else if (mutation.type === 'attributes') {
496 | _nodesCount++
497 | translateEl(mutation.target)
498 | } else {
499 | mutation.addedNodes.forEach(node => {
500 | if (node.className === 'bilingual__trans_wrapper') return
501 |
502 | _nodesCount++
503 | if (node.nodeType === 1 && /(output|gradio)-(html|markdown)/.test(node.className)) {
504 | translateEl(node, { rich: true })
505 | } else if (node.nodeType === 3) {
506 | doTranslate(node, node.textContent, 'text')
507 | } else {
508 | translateEl(node, { deep: true })
509 | }
510 | })
511 | }
512 | }
513 |
514 | if (_nodesCount > 0) {
515 | logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations)
516 | }
517 |
518 | if (loaded) return;
519 | if (i18n) return;
520 |
521 | loaded = true
522 | setup()
523 | })
524 |
525 | observer.observe(gradioApp(), {
526 | characterData: true,
527 | childList: true,
528 | subtree: true,
529 | attributes: true,
530 | attributeFilter: ['title', 'placeholder']
531 | })
532 | }
533 |
534 | // Init after page loaded
535 | document.addEventListener('DOMContentLoaded', init)
536 | })();
537 |
--------------------------------------------------------------------------------