I18N
I18N
This commit is contained in:
parent
e4afe5819b
commit
9183cc0c71
@ -6,6 +6,10 @@ import fcbh.cli_args as fcbh_cli
|
||||
fcbh_cli.parser.add_argument("--share", action='store_true', help="Set whether to share on Gradio.")
|
||||
fcbh_cli.parser.add_argument("--preset", type=str, default=None, help="Apply specified UI preset.")
|
||||
|
||||
fcbh_cli.parser.add_argument("--language", type=str, default=None,
|
||||
help="Translate UI using json files in [language] folder. "
|
||||
"For example, [--language example] will use [language/example.json] for translation.")
|
||||
|
||||
fcbh_cli.args = fcbh_cli.parser.parse_args()
|
||||
fcbh_cli.args.disable_cuda_malloc = True
|
||||
fcbh_cli.args.auto_launch = True
|
||||
|
@ -1 +1 @@
|
||||
version = '2.1.718'
|
||||
version = '2.1.719'
|
||||
|
205
javascript/localization.js
Normal file
205
javascript/localization.js
Normal file
@ -0,0 +1,205 @@
|
||||
|
||||
// localization = {} -- the dict with translations is created by the backend
|
||||
|
||||
var ignore_ids_for_localization = {
|
||||
setting_sd_hypernetwork: 'OPTION',
|
||||
setting_sd_model_checkpoint: 'OPTION',
|
||||
modelmerger_primary_model_name: 'OPTION',
|
||||
modelmerger_secondary_model_name: 'OPTION',
|
||||
modelmerger_tertiary_model_name: 'OPTION',
|
||||
train_embedding: 'OPTION',
|
||||
train_hypernetwork: 'OPTION',
|
||||
txt2img_styles: 'OPTION',
|
||||
img2img_styles: 'OPTION',
|
||||
setting_random_artist_categories: 'OPTION',
|
||||
setting_face_restoration_model: 'OPTION',
|
||||
setting_realesrgan_enabled_models: 'OPTION',
|
||||
extras_upscaler_1: 'OPTION',
|
||||
extras_upscaler_2: 'OPTION',
|
||||
};
|
||||
|
||||
var re_num = /^[.\d]+$/;
|
||||
var re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u;
|
||||
|
||||
var original_lines = {};
|
||||
var translated_lines = {};
|
||||
|
||||
function hasLocalization() {
|
||||
return window.localization && Object.keys(window.localization).length > 0;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var parentType = node.parentElement.nodeName;
|
||||
if (parentType == 'SCRIPT' || parentType == 'STYLE' || parentType == 'TEXTAREA') return false;
|
||||
|
||||
if (parentType == 'OPTION' || parentType == 'SPAN') {
|
||||
var 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;
|
||||
}
|
||||
|
||||
var tl = localization[text];
|
||||
if (tl !== undefined) {
|
||||
translated_lines[tl] = 1;
|
||||
}
|
||||
|
||||
return tl;
|
||||
}
|
||||
|
||||
function processTextNode(node) {
|
||||
var text = node.textContent.trim();
|
||||
|
||||
if (!canBeTranslated(node, text)) return;
|
||||
|
||||
var tl = getTranslation(text);
|
||||
if (tl !== undefined) {
|
||||
node.textContent = tl;
|
||||
}
|
||||
}
|
||||
|
||||
function processNode(node) {
|
||||
if (node.nodeType == 3) {
|
||||
processTextNode(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.title) {
|
||||
let tl = getTranslation(node.title);
|
||||
if (tl !== undefined) {
|
||||
node.title = tl;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.placeholder) {
|
||||
let tl = getTranslation(node.placeholder);
|
||||
if (tl !== undefined) {
|
||||
node.placeholder = tl;
|
||||
}
|
||||
}
|
||||
|
||||
textNodesUnder(node).forEach(function(node) {
|
||||
processTextNode(node);
|
||||
});
|
||||
}
|
||||
|
||||
function localizeWholePage() {
|
||||
processNode(gradioApp());
|
||||
|
||||
function elem(comp) {
|
||||
var elem_id = comp.props.elem_id ? comp.props.elem_id : "component-" + comp.id;
|
||||
return gradioApp().getElementById(elem_id);
|
||||
}
|
||||
|
||||
for (var comp of window.gradio_config.components) {
|
||||
if (comp.props.webui_tooltip) {
|
||||
let e = elem(comp);
|
||||
|
||||
let tl = e ? getTranslation(e.title) : undefined;
|
||||
if (tl !== undefined) {
|
||||
e.title = tl;
|
||||
}
|
||||
}
|
||||
if (comp.props.placeholder) {
|
||||
let e = elem(comp);
|
||||
let textbox = e ? e.querySelector('[placeholder]') : null;
|
||||
|
||||
let tl = textbox ? getTranslation(textbox.placeholder) : undefined;
|
||||
if (tl !== undefined) {
|
||||
textbox.placeholder = tl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dumpTranslations() {
|
||||
if (!hasLocalization()) {
|
||||
// If we don't have any localization,
|
||||
// we will not have traversed the app to find
|
||||
// original_lines, so do that now.
|
||||
localizeWholePage();
|
||||
}
|
||||
var dumped = {};
|
||||
if (localization.rtl) {
|
||||
dumped.rtl = true;
|
||||
}
|
||||
|
||||
for (const text in original_lines) {
|
||||
if (dumped[text] !== undefined) continue;
|
||||
dumped[text] = localization[text] || text;
|
||||
}
|
||||
|
||||
return dumped;
|
||||
}
|
||||
|
||||
function download_localization() {
|
||||
var 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);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (!hasLocalization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUiUpdate(function(m) {
|
||||
m.forEach(function(mutation) {
|
||||
mutation.addedNodes.forEach(function(node) {
|
||||
processNode(node);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
localizeWholePage();
|
||||
|
||||
if (localization.rtl) { // if the language is from right to left,
|
||||
(new MutationObserver((mutations, observer) => { // wait for the style to load
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.tagName === 'STYLE') {
|
||||
observer.disconnect();
|
||||
|
||||
for (const x of node.sheet.rules) { // find all rtl media rules
|
||||
if (Array.from(x.media || []).includes('rtl')) {
|
||||
x.media.appendMedium('all'); // enable them
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})).observe(gradioApp(), {childList: true});
|
||||
}
|
||||
});
|
@ -1,5 +1,4 @@
|
||||
// based on https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/v1.6.0/script.js
|
||||
|
||||
function gradioApp() {
|
||||
const elems = document.getElementsByTagName('gradio-app');
|
||||
const elem = elems.length == 0 ? document : elems[0];
|
||||
@ -12,22 +11,162 @@ function gradioApp() {
|
||||
return elem.shadowRoot ? elem.shadowRoot : elem;
|
||||
}
|
||||
|
||||
function playNotification() {
|
||||
gradioApp().querySelector('#audio_notification audio')?.play();
|
||||
/**
|
||||
* Get the currently selected top-level UI tab button (e.g. the button that says "Extras").
|
||||
*/
|
||||
function get_uiCurrentTab() {
|
||||
return gradioApp().querySelector('#tabs > .tab-nav > button.selected');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var handled = false;
|
||||
if (e.key !== undefined) {
|
||||
if ((e.key == "Enter" && (e.metaKey || e.ctrlKey || e.altKey))) handled = true;
|
||||
} else if (e.keyCode !== undefined) {
|
||||
if ((e.keyCode == 13 && (e.metaKey || e.ctrlKey || e.altKey))) handled = true;
|
||||
/**
|
||||
* Get the first currently visible top-level UI tab content (e.g. the div hosting the "txt2img" UI).
|
||||
*/
|
||||
function get_uiCurrentTabContent() {
|
||||
return gradioApp().querySelector('#tabs > .tabitem[id^=tab_]:not([style*="display: none"])');
|
||||
}
|
||||
|
||||
var uiUpdateCallbacks = [];
|
||||
var uiAfterUpdateCallbacks = [];
|
||||
var uiLoadedCallbacks = [];
|
||||
var uiTabChangeCallbacks = [];
|
||||
var optionsChangedCallbacks = [];
|
||||
var uiAfterUpdateTimeout = null;
|
||||
var uiCurrentTab = null;
|
||||
|
||||
/**
|
||||
* Register callback to be called at each UI update.
|
||||
* The callback receives an array of MutationRecords as an argument.
|
||||
*/
|
||||
function onUiUpdate(callback) {
|
||||
uiUpdateCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback to be called soon after UI updates.
|
||||
* The callback receives no arguments.
|
||||
*
|
||||
* This is preferred over `onUiUpdate` if you don't need
|
||||
* access to the MutationRecords, as your function will
|
||||
* not be called quite as often.
|
||||
*/
|
||||
function onAfterUiUpdate(callback) {
|
||||
uiAfterUpdateCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback to be called when the UI is loaded.
|
||||
* The callback receives no arguments.
|
||||
*/
|
||||
function onUiLoaded(callback) {
|
||||
uiLoadedCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback to be called when the UI tab is changed.
|
||||
* The callback receives no arguments.
|
||||
*/
|
||||
function onUiTabChange(callback) {
|
||||
uiTabChangeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register callback to be called when the options are changed.
|
||||
* The callback receives no arguments.
|
||||
* @param callback
|
||||
*/
|
||||
function onOptionsChanged(callback) {
|
||||
optionsChangedCallbacks.push(callback);
|
||||
}
|
||||
|
||||
function executeCallbacks(queue, arg) {
|
||||
for (const callback of queue) {
|
||||
try {
|
||||
callback(arg);
|
||||
} catch (e) {
|
||||
console.error("error running callback", callback, ":", e);
|
||||
}
|
||||
}
|
||||
if (handled) {
|
||||
var button = gradioApp().querySelector('button[id=generate_button]');
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the execution of the callbacks registered with onAfterUiUpdate.
|
||||
* The callbacks are executed after a short while, unless another call to this function
|
||||
* is made before that time. IOW, the callbacks are executed only once, even
|
||||
* when there are multiple mutations observed.
|
||||
*/
|
||||
function scheduleAfterUiUpdateCallbacks() {
|
||||
clearTimeout(uiAfterUpdateTimeout);
|
||||
uiAfterUpdateTimeout = setTimeout(function() {
|
||||
executeCallbacks(uiAfterUpdateCallbacks);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
var executedOnLoaded = false;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var mutationObserver = new MutationObserver(function(m) {
|
||||
if (!executedOnLoaded && gradioApp().querySelector('#txt2img_prompt')) {
|
||||
executedOnLoaded = true;
|
||||
executeCallbacks(uiLoadedCallbacks);
|
||||
}
|
||||
|
||||
executeCallbacks(uiUpdateCallbacks, m);
|
||||
scheduleAfterUiUpdateCallbacks();
|
||||
const newTab = get_uiCurrentTab();
|
||||
if (newTab && (newTab !== uiCurrentTab)) {
|
||||
uiCurrentTab = newTab;
|
||||
executeCallbacks(uiTabChangeCallbacks);
|
||||
}
|
||||
});
|
||||
mutationObserver.observe(gradioApp(), {childList: true, subtree: true});
|
||||
});
|
||||
|
||||
/**
|
||||
* Add a ctrl+enter as a shortcut to start a generation
|
||||
*/
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === 'Enter' || e.keyCode === 13;
|
||||
const isModifierKey = e.metaKey || e.ctrlKey || e.altKey;
|
||||
|
||||
const interruptButton = get_uiCurrentTabContent().querySelector('button[id$=_interrupt]');
|
||||
const generateButton = get_uiCurrentTabContent().querySelector('button[id$=_generate]');
|
||||
|
||||
if (isEnter && isModifierKey) {
|
||||
if (interruptButton.style.display === 'block') {
|
||||
interruptButton.click();
|
||||
setTimeout(function() {
|
||||
generateButton.click();
|
||||
}, 500);
|
||||
} else {
|
||||
generateButton.click();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* checks that a UI element is not in another hidden element or tab content
|
||||
*/
|
||||
function uiElementIsVisible(el) {
|
||||
if (el === document) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const computedStyle = getComputedStyle(el);
|
||||
const isVisible = computedStyle.display !== 'none';
|
||||
|
||||
if (!isVisible) return false;
|
||||
return uiElementIsVisible(el.parentNode);
|
||||
}
|
||||
|
||||
function uiElementInSight(el) {
|
||||
const clRect = el.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight;
|
||||
const isOnScreen = clRect.bottom > 0 && clRect.top < windowHeight;
|
||||
|
||||
return isOnScreen;
|
||||
}
|
||||
|
||||
function playNotification() {
|
||||
gradioApp().querySelector('#audio_notification audio')?.play();
|
||||
}
|
||||
|
6
language/example.json
Normal file
6
language/example.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"Generate": "生成",
|
||||
"Input Image": "入力画像",
|
||||
"Advanced": "고급",
|
||||
"SAI 3D Model": "SAI 3D Modèle"
|
||||
}
|
25
modules/localization.py
Normal file
25
modules/localization.py
Normal file
@ -0,0 +1,25 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
localization_root = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'language')
|
||||
|
||||
|
||||
def localization_js(filename):
|
||||
data = {}
|
||||
|
||||
if isinstance(filename, str):
|
||||
full_name = os.path.abspath(os.path.join(localization_root, filename + '.json'))
|
||||
if os.path.exists(full_name):
|
||||
try:
|
||||
with open(full_name, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
assert isinstance(data, dict)
|
||||
for k, v in data.items():
|
||||
assert isinstance(k, str)
|
||||
assert isinstance(v, str)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
print(f'Failed to load localization file {full_name}')
|
||||
|
||||
return f"window.localization = {json.dumps(data)}"
|
@ -2,6 +2,10 @@
|
||||
|
||||
import os
|
||||
import gradio as gr
|
||||
import args_manager
|
||||
|
||||
from modules.localization import localization_js
|
||||
|
||||
|
||||
GradioTemplateResponseOriginal = gr.routes.templates.TemplateResponse
|
||||
|
||||
@ -21,8 +25,11 @@ def webpath(fn):
|
||||
def javascript_html():
|
||||
script_js_path = webpath('javascript/script.js')
|
||||
context_menus_js_path = webpath('javascript/contextMenus.js')
|
||||
head = f'<script type="text/javascript" src="{script_js_path}"></script>\n'
|
||||
localization_js_path = webpath('javascript/localization.js')
|
||||
head = f'<script type="text/javascript">{localization_js(args_manager.args.language)}</script>\n'
|
||||
head += f'<script type="text/javascript" src="{script_js_path}"></script>\n'
|
||||
head += f'<script type="text/javascript" src="{context_menus_js_path}"></script>\n'
|
||||
head += f'<script type="text/javascript" src="{localization_js_path}"></script>\n'
|
||||
return head
|
||||
|
||||
|
||||
|
33
readme.md
33
readme.md
@ -296,3 +296,36 @@ Special thanks to [twri](https://github.com/twri) and [3Diva](https://github.com
|
||||
## Update Log
|
||||
|
||||
The log is [here](update_log.md).
|
||||
|
||||
# Localization/Translation/I18N
|
||||
|
||||
**We need your help!** Please help with translating Fooocus to international languages.
|
||||
|
||||
You can put json files in the `language` folder to translate the user interface.
|
||||
|
||||
For example, below is the content of `Fooocus/language/example.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Generate": "生成",
|
||||
"Input Image": "入力画像",
|
||||
"Advanced": "고급",
|
||||
"SAI 3D Model": "SAI 3D Modèle"
|
||||
}
|
||||
```
|
||||
|
||||
If you add `--language example` arg, Fooocus will read `Fooocus/language/example.json` to translate the UI.
|
||||
|
||||
For example, you can edit the ending line of Windows `run.bat` as
|
||||
|
||||
.\python_embeded\python.exe -s Fooocus\entry_with_update.py --language example
|
||||
|
||||
Or `run_anime.bat` as
|
||||
|
||||
.\python_embeded\python.exe -s Fooocus\entry_with_update.py --language example --preset anime
|
||||
|
||||
Or `run_realistic.bat` as
|
||||
|
||||
.\python_embeded\python.exe -s Fooocus\entry_with_update.py --language example --preset realistic
|
||||
|
||||
For practical translation, you may create your own file like `Fooocus/language/jp.json` or `Fooocus/language/cn.json` and then use flag `--language jp` or `--language cn`. Apparently, these files do not exist now. **We need your help to create these files!**
|
||||
|
@ -1,3 +1,7 @@
|
||||
# 2.1.719
|
||||
|
||||
* I18N
|
||||
|
||||
# 2.1.718
|
||||
|
||||
* Corrected handling dash in wildcard names, more wildcards (extended-color).
|
||||
|
Loading…
Reference in New Issue
Block a user