浏览代码

localization support

AUTOMATIC 2 年之前
父节点
当前提交
cf47d13c1e
共有 7 个文件被更改,包括 211 次插入18 次删除
  1. 146 0
      javascript/localization.js
  2. 0 0
      localizations/Put localization files here.txt
  3. 31 0
      modules/localization.py
  4. 5 2
      modules/shared.py
  5. 23 10
      modules/ui.py
  6. 5 5
      script.js
  7. 1 1
      style.css

+ 146 - 0
javascript/localization.js

@@ -0,0 +1,146 @@
+
+// localization = {} -- the dict with translations is created by the backend
+
+ignore_ids_for_localization={
+    setting_sd_hypernetwork: 'OPTION',
+    setting_sd_model_checkpoint: 'OPTION',
+    setting_realesrgan_enabled_models: 'OPTION',
+    modelmerger_primary_model_name: 'OPTION',
+    modelmerger_secondary_model_name: 'OPTION',
+    modelmerger_tertiary_model_name: 'OPTION',
+    train_embedding: 'OPTION',
+    train_hypernetwork: 'OPTION',
+    txt2img_style_index: 'OPTION',
+    txt2img_style2_index: 'OPTION',
+    img2img_style_index: 'OPTION',
+    img2img_style2_index: 'OPTION',
+    setting_random_artist_categories: 'SPAN',
+    setting_face_restoration_model: 'SPAN',
+    setting_realesrgan_enabled_models: 'SPAN',
+    extras_upscaler_1: 'SPAN',
+    extras_upscaler_2: 'SPAN',
+}
+
+re_num = /^[\.\d]+$/
+re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
+
+original_lines = {}
+translated_lines = {}
+
+function textNodesUnder(el){
+    var n, a=[], walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false);
+    while(n=walk.nextNode()) a.push(n);
+    return a;
+}
+
+function canBeTranslated(node, text){
+    if(! text) return false;
+    if(! node.parentElement) return false;
+
+    parentType = node.parentElement.nodeName
+    if(parentType=='SCRIPT' || parentType=='STYLE' || parentType=='TEXTAREA') return false;
+
+    if (parentType=='OPTION' || parentType=='SPAN'){
+        pnode = node
+        for(var level=0; level<4; level++){
+            pnode = pnode.parentElement
+            if(! pnode) break;
+
+            if(ignore_ids_for_localization[pnode.id] == parentType) return false;
+        }
+    }
+
+    if(re_num.test(text)) return false;
+    if(re_emoji.test(text)) return false;
+    return true
+}
+
+function getTranslation(text){
+    if(! text) return undefined
+
+    if(translated_lines[text] === undefined){
+        original_lines[text] = 1
+    }
+
+    tl = localization[text]
+    if(tl !== undefined){
+        translated_lines[tl] = 1
+    }
+
+    return tl
+}
+
+function processTextNode(node){
+    text = node.textContent.trim()
+
+    if(! canBeTranslated(node, text)) return
+
+    tl = getTranslation(text)
+    if(tl !== undefined){
+        node.textContent = tl
+    }
+}
+
+function processNode(node){
+    if(node.nodeType == 3){
+        processTextNode(node)
+        return
+    }
+
+    if(node.title){
+        tl = getTranslation(node.title)
+        if(tl !== undefined){
+            node.title = tl
+        }
+    }
+
+    if(node.placeholder){
+        tl = getTranslation(node.placeholder)
+        if(tl !== undefined){
+            node.placeholder = tl
+        }
+    }
+
+    textNodesUnder(node).forEach(function(node){
+        processTextNode(node)
+    })
+}
+
+function dumpTranslations(){
+    dumped = {}
+
+    Object.keys(original_lines).forEach(function(text){
+        if(dumped[text] !== undefined)  return
+
+        dumped[text] = localization[text] || text
+    })
+
+    return dumped
+}
+
+onUiUpdate(function(m){
+    m.forEach(function(mutation){
+        mutation.addedNodes.forEach(function(node){
+            processNode(node)
+        })
+    });
+})
+
+
+document.addEventListener("DOMContentLoaded", function() {
+    processNode(gradioApp())
+})
+
+function download_localization() {
+    text = JSON.stringify(dumpTranslations(), null, 4)
+
+    var element = document.createElement('a');
+    element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
+    element.setAttribute('download', "localization.json");
+    element.style.display = 'none';
+    document.body.appendChild(element);
+
+    element.click();
+
+    document.body.removeChild(element);
+}

+ 0 - 0
localizations/Put localization files here.txt


+ 31 - 0
modules/localization.py

@@ -0,0 +1,31 @@
+import json
+import os
+import sys
+import traceback
+
+localizations = {}
+
+
+def list_localizations(dirname):
+    localizations.clear()
+
+    for file in os.listdir(dirname):
+        fn, ext = os.path.splitext(file)
+        if ext.lower() != ".json":
+            continue
+
+        localizations[fn] = os.path.join(dirname, file)
+
+
+def localization_js(current_localization_name):
+    fn = localizations.get(current_localization_name, None)
+    data = {}
+    if fn is not None:
+        try:
+            with open(fn, "r", encoding="utf8") as file:
+                data = json.load(file)
+        except Exception:
+            print(f"Error loading localization from {fn}:", file=sys.stderr)
+            print(traceback.format_exc(), file=sys.stderr)
+
+    return f"var localization = {json.dumps(data)}\n"

+ 5 - 2
modules/shared.py

@@ -13,7 +13,7 @@ import modules.memmon
 import modules.sd_models
 import modules.styles
 import modules.devices as devices
-from modules import sd_samplers, sd_models
+from modules import sd_samplers, sd_models, localization
 from modules.hypernetworks import hypernetwork
 from modules.paths import models_path, script_path, sd_path
 
@@ -31,6 +31,7 @@ parser.add_argument("--no-progressbar-hiding", action='store_true', help="do not
 parser.add_argument("--max-batch-count", type=int, default=16, help="maximum batch count value for the UI")
 parser.add_argument("--embeddings-dir", type=str, default=os.path.join(script_path, 'embeddings'), help="embeddings directory for textual inversion (default: embeddings)")
 parser.add_argument("--hypernetwork-dir", type=str, default=os.path.join(models_path, 'hypernetworks'), help="hypernetwork directory")
+parser.add_argument("--localizations-dir", type=str, default=os.path.join(script_path, 'localizations'), help="localizations directory")
 parser.add_argument("--allow-code", action='store_true', help="allow custom script execution from webui")
 parser.add_argument("--medvram", action='store_true', help="enable stable diffusion model optimizations for sacrificing a little speed for low VRM usage")
 parser.add_argument("--lowvram", action='store_true', help="enable stable diffusion model optimizations for sacrificing a lot of speed for very low VRM usage")
@@ -103,7 +104,6 @@ os.makedirs(cmd_opts.hypernetwork_dir, exist_ok=True)
 hypernetworks = hypernetwork.list_hypernetworks(cmd_opts.hypernetwork_dir)
 loaded_hypernetwork = None
 
-
 def reload_hypernetworks():
     global hypernetworks
 
@@ -151,6 +151,8 @@ interrogator = modules.interrogate.InterrogateModels("interrogate")
 
 face_restorers = []
 
+localization.list_localizations(cmd_opts.localizations_dir)
+
 
 def realesrgan_models_names():
     import modules.realesrgan_model
@@ -296,6 +298,7 @@ options_templates.update(options_section(('ui', "User interface"), {
     "js_modal_lightbox_initially_zoomed": OptionInfo(True, "Show images zoomed in by default in full page image viewer"),
     "show_progress_in_title": OptionInfo(True, "Show generation progress in window title."),
     'quicksettings': OptionInfo("sd_model_checkpoint", "Quicksettings list"),
+    'localization': OptionInfo("None", "Localization (requires restart)", gr.Dropdown, lambda: {"choices": ["None"] + list(localization.localizations.keys())}, refresh=lambda: localization.list_localizations(cmd_opts.localizations_dir)),
 }))
 
 options_templates.update(options_section(('sampler-params', "Sampler parameters"), {

+ 23 - 10
modules/ui.py

@@ -23,7 +23,7 @@ import gradio as gr
 import gradio.utils
 import gradio.routes
 
-from modules import sd_hijack, sd_models
+from modules import sd_hijack, sd_models, localization
 from modules.paths import script_path
 from modules.shared import opts, cmd_opts, restricted_opts
 if cmd_opts.deepdanbooru:
@@ -1056,10 +1056,10 @@ def create_ui(wrap_gradio_gpu_call):
                             upscaling_crop = gr.Checkbox(label='Crop to fit', value=True)
 
                 with gr.Group():
-                    extras_upscaler_1 = gr.Radio(label='Upscaler 1', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index")
+                    extras_upscaler_1 = gr.Radio(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index")
 
                 with gr.Group():
-                    extras_upscaler_2 = gr.Radio(label='Upscaler 2', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index")
+                    extras_upscaler_2 = gr.Radio(label='Upscaler 2', celem_id="extras_upscaler_2", hoices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index")
                     extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=1)
 
                 with gr.Group():
@@ -1224,10 +1224,10 @@ def create_ui(wrap_gradio_gpu_call):
                 with gr.Tab(label="Train"):
                     gr.HTML(value="<p style='margin-bottom: 0.7em'>Train an embedding; must specify a directory with a set of 1:1 ratio images</p>")
                     with gr.Row():
-                        train_embedding_name = gr.Dropdown(label='Embedding', choices=sorted(sd_hijack.model_hijack.embedding_db.word_embeddings.keys()))
+                        train_embedding_name = gr.Dropdown(label='Embedding', elem_id="train_embedding", choices=sorted(sd_hijack.model_hijack.embedding_db.word_embeddings.keys()))
                         create_refresh_button(train_embedding_name, sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings, lambda: {"choices": sorted(sd_hijack.model_hijack.embedding_db.word_embeddings.keys())}, "refresh_train_embedding_name")
                     with gr.Row():
-                        train_hypernetwork_name = gr.Dropdown(label='Hypernetwork', choices=[x for x in shared.hypernetworks.keys()])
+                        train_hypernetwork_name = gr.Dropdown(label='Hypernetwork', elem_id="train_hypernetwork", choices=[x for x in shared.hypernetworks.keys()])
                         create_refresh_button(train_hypernetwork_name, shared.reload_hypernetworks, lambda: {"choices": sorted([x for x in shared.hypernetworks.keys()])}, "refresh_train_hypernetwork_name")
                     learn_rate = gr.Textbox(label='Learning rate', placeholder="Learning rate", value="0.005")
                     batch_size = gr.Number(label='Batch size', value=1, precision=0)
@@ -1376,16 +1376,18 @@ def create_ui(wrap_gradio_gpu_call):
         else:
             raise Exception(f'bad options item type: {str(t)} for key {key}')
 
+        elem_id = "setting_"+key
+
         if info.refresh is not None:
             if is_quicksettings:
-                res = comp(label=info.label, value=fun, **(args or {}))
-                refresh_button = create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key)
+                res = comp(label=info.label, value=fun, elem_id=elem_id, **(args or {}))
+                create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key)
             else:
                 with gr.Row(variant="compact"):
-                    res = comp(label=info.label, value=fun, **(args or {}))
-                    refresh_button = create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key)
+                    res = comp(label=info.label, value=fun, elem_id=elem_id, **(args or {}))
+                    create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key)
         else:
-            res = comp(label=info.label, value=fun, **(args or {}))
+            res = comp(label=info.label, value=fun, elem_id=elem_id, **(args or {}))
 
 
         return res
@@ -1509,6 +1511,9 @@ Requested path was: {f}
 
         with gr.Row():
             request_notifications = gr.Button(value='Request browser notifications', elem_id="request_notifications")
+            download_localization = gr.Button(value='Download localization template', elem_id="download_localization")
+
+        with gr.Row():
             reload_script_bodies = gr.Button(value='Reload custom script bodies (No ui updates, No restart)', variant='secondary')
             restart_gradio = gr.Button(value='Restart Gradio and Refresh components (Custom Scripts, ui.py, js and css only)', variant='primary')
 
@@ -1519,6 +1524,13 @@ Requested path was: {f}
             _js='function(){}'
         )
 
+        download_localization.click(
+            fn=lambda: None,
+            inputs=[],
+            outputs=[],
+            _js='download_localization'
+        )
+
         def reload_scripts():
             modules.scripts.reload_script_body_only()
 
@@ -1784,6 +1796,7 @@ for filename in sorted(os.listdir(jsdir)):
     with open(os.path.join(jsdir, filename), "r", encoding="utf8") as jsfile:
         javascript += f"\n<script>{jsfile.read()}</script>"
 
+javascript += f"\n<script>{localization.localization_js(shared.opts.localization)}</script>"
 
 if 'gradio_routes_templates_response' not in globals():
     def template_response(*args, **kwargs):

+ 5 - 5
script.js

@@ -21,20 +21,20 @@ function onUiTabChange(callback){
     uiTabChangeCallbacks.push(callback)
 }
 
-function runCallback(x){
+function runCallback(x, m){
     try {
-        x()
+        x(m)
     } catch (e) {
         (console.error || console.log).call(console, e.message, e);
     }
 }
-function executeCallbacks(queue) {
-    queue.forEach(runCallback)
+function executeCallbacks(queue, m) {
+    queue.forEach(function(x){runCallback(x, m)})
 }
 
 document.addEventListener("DOMContentLoaded", function() {
     var mutationObserver = new MutationObserver(function(m){
-        executeCallbacks(uiUpdateCallbacks);
+        executeCallbacks(uiUpdateCallbacks, m);
         const newTab = get_uiCurrentTab();
         if ( newTab && ( newTab !== uiCurrentTab ) ) {
             uiCurrentTab = newTab;

+ 1 - 1
style.css

@@ -478,7 +478,7 @@ input[type="range"]{
     padding: 0;
 }
 
-#refresh_sd_model_checkpoint, #refresh_sd_hypernetwork, #refresh_train_hypernetwork_name, #refresh_train_embedding_name{
+#refresh_sd_model_checkpoint, #refresh_sd_hypernetwork, #refresh_train_hypernetwork_name, #refresh_train_embedding_name, #refresh_localization{
     max-width: 2.5em;
     min-width: 2.5em;
     height: 2.4em;