I18N
This commit is contained in:
lllyasviel 2023-10-20 07:33:11 -07:00
parent e4afe5819b
commit 9183cc0c71
9 changed files with 438 additions and 15 deletions

View File

@ -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

View File

@ -1 +1 @@
version = '2.1.718'
version = '2.1.719'

205
javascript/localization.js Normal file
View 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});
}
});

View File

@ -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
View File

@ -0,0 +1,6 @@
{
"Generate": "生成",
"Input Image": "入力画像",
"Advanced": "고급",
"SAI 3D Model": "SAI 3D Modèle"
}

25
modules/localization.py Normal file
View 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)}"

View File

@ -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

View File

@ -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!**

View File

@ -1,3 +1,7 @@
# 2.1.719
* I18N
# 2.1.718
* Corrected handling dash in wildcard names, more wildcards (extended-color).