├── .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 |

sd-webui-bilingual-localization

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 (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 | ![Snipaste_2023-02-28_00-27-48](https://user-images.githubusercontent.com/16256221/221625310-a6ef0b4c-a1e0-46bb-be9c-6d88cd0ad684.png) 28 | 29 | 之后切换到Installed面板,点击Apply and restart UI按钮 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 | > 确保Settings - User interface - Localization 已设置为了 `None` 45 | 46 | 在Settings - Bilingual Localization中选择要启用的本地化文件,依次点击Apply settingsReload UI按钮 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 | 本地化语料支持限定作用域,防止影响全局翻译,语法规则: 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 |

sd-webui-bilingual-localization

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(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 | ![Snipaste_2023-02-28_00-27-48](https://user-images.githubusercontent.com/16256221/221625310-a6ef0b4c-a1e0-46bb-be9c-6d88cd0ad684.png) 30 | 31 | その後、Installedパネルに切り替え、Apply and restart UIボタンをクリックします。 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 | > Settings - User interface - Localizationが`None`に設定されていることを確認してください。 48 | 49 | Settings - Bilingual Localizationパネルで、有効にしたい言語ファイル名を選択し、Apply settingsボタンとReload UIボタンを順にクリックします。 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 | ローカリゼーションは、グローバルな影響を防ぐためにスコープを限定したサポートを提供します。構文規則は以下の通りです: 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 |

sd-webui-bilingual-localization

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 (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 | ![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 Installed panel and click the Apply and restart UI 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 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 | ![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 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 | --------------------------------------------------------------------------------