chen55556 commited on
Commit
c031791
·
1 Parent(s): 6bd6690

Upload 7 files

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