|
(function () { |
|
const customCSS = ` |
|
.bilingual__trans_wrapper { |
|
display: inline-flex; |
|
flex-direction: column; |
|
align-items: center; |
|
font-size: 13px; |
|
line-height: 1; |
|
} |
|
|
|
.bilingual__trans_wrapper em { |
|
font-style: normal; |
|
} |
|
|
|
#txtimg_hr_finalres .bilingual__trans_wrapper em, |
|
#tab_ti .output-html .bilingual__trans_wrapper em, |
|
#tab_ti .gradio-html .bilingual__trans_wrapper em, |
|
#sddp-dynamic-prompting .gradio-html .bilingual__trans_wrapper em, |
|
#available_extensions .extension-tag .bilingual__trans_wrapper em, |
|
#available_extensions .date_added .bilingual__trans_wrapper em, |
|
#available_extensions+p>.bilingual__trans_wrapper em, |
|
.gradio-image div[data-testid="image"] .bilingual__trans_wrapper em { |
|
display: none; |
|
} |
|
|
|
#settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper), |
|
label>span>.bilingual__trans_wrapper, |
|
fieldset>span>.bilingual__trans_wrapper, |
|
.label-wrap>span>.bilingual__trans_wrapper, |
|
.w-full>span>.bilingual__trans_wrapper, |
|
.context-menu-items .bilingual__trans_wrapper, |
|
.single-select .bilingual__trans_wrapper, ul.options .inner-item + .bilingual__trans_wrapper, |
|
.output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper), |
|
.gradio-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper, .posex_cont .bilingual__trans_wrapper), |
|
.output-markdown .bilingual__trans_wrapper, |
|
.gradio-markdown .bilingual__trans_wrapper, |
|
.gradio-image>div.float .bilingual__trans_wrapper, |
|
.gradio-file>div.float .bilingual__trans_wrapper, |
|
.gradio-code>div.float .bilingual__trans_wrapper, |
|
.posex_setting_cont .bilingual__trans_wrapper:not(.posex_bg .bilingual__trans_wrapper), /* Posex extension */ |
|
#dynamic-prompting .bilingual__trans_wrapper |
|
{ |
|
font-size: 12px; |
|
align-items: flex-start; |
|
} |
|
|
|
#extensions label .bilingual__trans_wrapper, |
|
#available_extensions td .bilingual__trans_wrapper, |
|
.label-wrap>span>.bilingual__trans_wrapper { |
|
font-size: inherit; |
|
line-height: inherit; |
|
} |
|
|
|
.label-wrap>span:first-of-type { |
|
font-size: 13px; |
|
line-height: 1; |
|
} |
|
|
|
#txt2img_script_container > div { |
|
margin-top: var(--layout-gap, 12px); |
|
} |
|
|
|
textarea::placeholder { |
|
line-height: 1; |
|
padding: 4px 0; |
|
} |
|
|
|
label>span { |
|
line-height: 1; |
|
} |
|
|
|
div[data-testid="image"] .start-prompt { |
|
background-color: rgba(255, 255, 255, .6); |
|
color: #222; |
|
transition: opacity .2s ease-in-out; |
|
} |
|
div[data-testid="image"]:hover .start-prompt { |
|
opacity: 0; |
|
} |
|
|
|
.label-wrap > span.icon { |
|
width: 1em; |
|
height: 1em; |
|
transform-origin: center center; |
|
} |
|
|
|
.gradio-dropdown ul.options li.item { |
|
padding: 0.3em 0.4em !important; |
|
} |
|
|
|
/* Posex extension */ |
|
.posex_bg { |
|
white-space: nowrap; |
|
} |
|
` |
|
|
|
let i18n = null, i18nRegex = {}, i18nScope = {}, scopedSource = {}, config = null; |
|
|
|
|
|
function setup() { |
|
config = { |
|
enabled: opts["bilingual_localization_enabled"], |
|
file: opts["bilingual_localization_file"], |
|
dirs: opts["bilingual_localization_dirs"], |
|
order: opts["bilingual_localization_order"], |
|
enableLogger: opts["bilingual_localization_logger"] |
|
} |
|
|
|
let { enabled, file, dirs, enableLogger } = config |
|
|
|
if (!enabled || file === "None" || dirs === "None") return |
|
|
|
dirs = JSON.parse(dirs) |
|
|
|
enableLogger && logger.init('Bilingual') |
|
logger.log('Bilingual Localization initialized.') |
|
|
|
|
|
const regex_scope = /^##(?<scope>\S+)##(?<skey>\S+)$/ |
|
i18n = JSON.parse(readFile(dirs[file]), (key, value) => { |
|
|
|
if (key.startsWith('@@')) { |
|
i18nRegex[key.slice(2)] = value |
|
} else if (regex_scope.test(key)) { |
|
|
|
const { scope, skey } = key.match(regex_scope).groups |
|
i18nScope[scope] ||= {} |
|
i18nScope[scope][skey] = value |
|
|
|
scopedSource[skey] ||= [] |
|
scopedSource[skey].push(scope) |
|
} else { |
|
return value |
|
} |
|
}) |
|
|
|
logger.group('Localization file loaded.') |
|
logger.log('i18n', i18n) |
|
logger.log('i18nRegex', i18nRegex) |
|
logger.log('i18nScope', i18nScope) |
|
logger.groupEnd() |
|
|
|
translatePage() |
|
handleDropdown() |
|
} |
|
|
|
function handleDropdown() { |
|
|
|
delegateEvent(gradioApp(), 'mousedown', 'ul.options .item', function (event) { |
|
const { target } = event |
|
|
|
if (!target.classList.contains('item')) { |
|
|
|
target.closest('.item').dispatchEvent(new Event('mousedown', { bubbles: true })) |
|
return |
|
} |
|
|
|
const source = target.dataset.value |
|
const $labelEl = target?.closest('.wrap')?.querySelector('.wrap-inner .single-select') |
|
|
|
if (source && $labelEl) { |
|
$labelEl.title = titles?.[source] || '' |
|
$labelEl.textContent = "__biligual__will_be_replaced__" |
|
doTranslate($labelEl, source, 'element') |
|
} |
|
}); |
|
} |
|
|
|
|
|
function translatePage() { |
|
if (!i18n) return |
|
|
|
logger.time('Full Page') |
|
querySelectorAll([ |
|
"label span, fieldset span, button", |
|
"textarea[placeholder], select, option", |
|
".transition > div > span:not([class])", ".label-wrap > span", |
|
".gradio-image>div.float", |
|
".gradio-file>div.float", |
|
".gradio-code>div.float", |
|
"#modelmerger_interp_description .output-html", |
|
"#modelmerger_interp_description .gradio-html", |
|
"#lightboxModal span" |
|
]) |
|
.forEach(el => translateEl(el, { deep: true })) |
|
|
|
querySelectorAll([ |
|
'div[data-testid="image"] > div > div', |
|
'#extras_image_batch > div', |
|
".output-html:not(#footer), .gradio-html:not(#footer), .output-markdown, .gradio-markdown", |
|
'#dynamic-prompting' |
|
]) |
|
.forEach(el => translateEl(el, { rich: true })) |
|
|
|
logger.timeEnd('Full Page') |
|
} |
|
|
|
const ignore_selector = [ |
|
'.bilingual__trans_wrapper', |
|
'.resultsFlexContainer', |
|
'#setting_sd_model_checkpoint select', |
|
'#setting_sd_vae select', |
|
'#txt2img_styles, #img2txt_styles', |
|
'.extra-network-cards .card .actions .name', |
|
'script, style, svg, g, path', |
|
] |
|
|
|
function translateEl(el, { deep = false, rich = false } = {}) { |
|
if (!i18n) return |
|
if (el.matches?.(ignore_selector)) return |
|
|
|
if (el.title) { |
|
doTranslate(el, el.title, 'title') |
|
} |
|
|
|
if (el.placeholder) { |
|
doTranslate(el, el.placeholder, 'placeholder') |
|
} |
|
|
|
if (el.tagName === 'OPTION') { |
|
doTranslate(el, el.textContent, 'option') |
|
} |
|
|
|
if (deep || rich) { |
|
Array.from(el.childNodes).forEach(node => { |
|
if (node.nodeName === '#text') { |
|
if (rich) { |
|
doTranslate(node, node.textContent, 'text') |
|
return |
|
} |
|
|
|
if (deep) { |
|
doTranslate(node, node.textContent, 'element') |
|
} |
|
} else if (node.childNodes.length > 0) { |
|
translateEl(node, { deep, rich }) |
|
} |
|
}) |
|
} else { |
|
doTranslate(el, el.textContent, 'element') |
|
} |
|
} |
|
|
|
function checkRegex(source) { |
|
for (let regex in i18nRegex) { |
|
regex = getRegex(regex) |
|
if (regex && regex.test(source)) { |
|
logger.log('regex', regex, source) |
|
return source.replace(regex, i18nRegex[regex]) |
|
} |
|
} |
|
} |
|
|
|
const re_num = /^[\.\d]+$/, |
|
re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u |
|
|
|
function doTranslate(el, source, type) { |
|
if (!i18n) return |
|
source = source.trim() |
|
if (!source) return |
|
if (re_num.test(source)) return |
|
|
|
|
|
let translation = i18n[source] || checkRegex(source), |
|
scopes = scopedSource[source] |
|
|
|
if (scopes) { |
|
console.log('scope', el, source); |
|
for (let scope of scopes) { |
|
if (el.parentElement.closest(`#${scope}`)) { |
|
translation = i18nScope[scope][source] |
|
break |
|
} |
|
} |
|
} |
|
|
|
if (!translation || source === translation) { |
|
if (el.textContent === '__biligual__will_be_replaced__') el.textContent = source |
|
if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() |
|
return |
|
} |
|
|
|
if (config.order === "Original First") { |
|
[source, translation] = [translation, source] |
|
} |
|
|
|
switch (type) { |
|
case 'text': |
|
el.textContent = translation |
|
break; |
|
|
|
case 'element': |
|
const htmlStr = `<div class="bilingual__trans_wrapper">${htmlEncode(translation)}<em>${htmlEncode(source)}</em></div>` |
|
const htmlEl = parseHtmlStringToElement(htmlStr) |
|
if (el.hasChildNodes()) { |
|
const textNode = Array.from(el.childNodes).find(node => |
|
node.nodeName === '#text' && |
|
(node.textContent.trim() === source || node.textContent.trim() === '__biligual__will_be_replaced__') |
|
) |
|
|
|
if (textNode) { |
|
textNode.textContent = '' |
|
if (textNode.nextSibling?.className === 'bilingual__trans_wrapper') textNode.nextSibling.remove() |
|
textNode.parentNode.insertBefore(htmlEl, textNode.nextSibling) |
|
} |
|
} else { |
|
el.textContent = '' |
|
if (el.nextSibling?.className === 'bilingual__trans_wrapper') el.nextSibling.remove() |
|
el.parentNode.insertBefore(htmlEl, el.nextSibling) |
|
} |
|
break; |
|
|
|
case 'option': |
|
el.textContent = `${translation} (${source})` |
|
break; |
|
|
|
case 'title': |
|
el.title = `${translation}\n${source}` |
|
break; |
|
|
|
case 'placeholder': |
|
el.placeholder = `${translation}\n\n${source}` |
|
break; |
|
|
|
default: |
|
return translation |
|
} |
|
} |
|
|
|
function gradioApp() { |
|
const elems = document.getElementsByTagName('gradio-app') |
|
const elem = elems.length == 0 ? document : elems[0] |
|
|
|
if (elem !== document) elem.getElementById = function (id) { return document.getElementById(id) } |
|
return elem.shadowRoot ? elem.shadowRoot : elem |
|
} |
|
|
|
function querySelector(...args) { |
|
return gradioApp()?.querySelector(...args) |
|
} |
|
|
|
function querySelectorAll(...args) { |
|
return gradioApp()?.querySelectorAll(...args) |
|
} |
|
|
|
function delegateEvent(parent, eventType, selector, handler) { |
|
parent.addEventListener(eventType, function (event) { |
|
var target = event.target; |
|
while (target !== parent) { |
|
if (target.matches(selector)) { |
|
handler.call(target, event); |
|
} |
|
target = target.parentNode; |
|
} |
|
}); |
|
} |
|
|
|
function parseHtmlStringToElement(htmlStr) { |
|
const template = document.createElement('template') |
|
template.insertAdjacentHTML('afterbegin', htmlStr) |
|
return template.firstElementChild |
|
} |
|
|
|
function htmlEncode(htmlStr) { |
|
return htmlStr.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') |
|
.replace(/"/g, '"').replace(/'/g, ''') |
|
} |
|
|
|
|
|
function getRegex(regex) { |
|
try { |
|
regex = regex.trim(); |
|
let parts = regex.split('/'); |
|
if (regex[0] !== '/' || parts.length < 3) { |
|
regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); |
|
return new RegExp(regex); |
|
} |
|
|
|
const option = parts[parts.length - 1]; |
|
const lastIndex = regex.lastIndexOf('/'); |
|
regex = regex.substring(1, lastIndex); |
|
return new RegExp(regex, option); |
|
} catch (e) { |
|
return null |
|
} |
|
} |
|
|
|
|
|
function readFile(filePath) { |
|
let request = new XMLHttpRequest(); |
|
request.open("GET", `file=${filePath}`, false); |
|
request.send(null); |
|
return request.responseText; |
|
} |
|
|
|
const logger = (function () { |
|
const loggerTimerMap = new Map() |
|
const loggerConf = { badge: true, label: 'Logger', enable: false } |
|
return new Proxy(console, { |
|
get: (target, propKey) => { |
|
if (propKey === 'init') { |
|
return (label) => { |
|
loggerConf.label = label |
|
loggerConf.enable = true |
|
} |
|
} |
|
|
|
if (!(propKey in target)) return undefined |
|
|
|
return (...args) => { |
|
if (!loggerConf.enable) return |
|
|
|
let color = ['#39cfe1', '#006cab'] |
|
|
|
let label, start |
|
switch (propKey) { |
|
case 'error': |
|
color = ['#f70000', '#a70000'] |
|
break; |
|
case 'warn': |
|
color = ['#f7b500', '#b58400'] |
|
break; |
|
case 'time': |
|
label = args[0] |
|
if (loggerTimerMap.has(label)) { |
|
logger.warn(`Timer '${label}' already exisits`) |
|
} else { |
|
loggerTimerMap.set(label, performance.now()) |
|
} |
|
return |
|
case 'timeEnd': |
|
label = args[0], start = loggerTimerMap.get(label) |
|
if (start === undefined) { |
|
logger.warn(`Timer '${label}' does not exist`) |
|
} else { |
|
loggerTimerMap.delete(label) |
|
logger.log(`${label}: ${performance.now() - start} ms`) |
|
} |
|
return |
|
case 'groupEnd': |
|
loggerConf.badge = true |
|
break |
|
} |
|
|
|
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;`] : [] |
|
|
|
target[propKey](...badge, ...args) |
|
|
|
if (propKey === 'group' || propKey === 'groupCollapsed') { |
|
loggerConf.badge = false |
|
} |
|
} |
|
} |
|
}) |
|
}()) |
|
|
|
function init() { |
|
|
|
let $styleEL = document.createElement('style'); |
|
|
|
if ($styleEL.styleSheet) { |
|
$styleEL.styleSheet.cssText = customCSS; |
|
} else { |
|
$styleEL.appendChild(document.createTextNode(customCSS)); |
|
} |
|
gradioApp().appendChild($styleEL); |
|
|
|
let loaded = false |
|
let _count = 0 |
|
|
|
const observer = new MutationObserver(mutations => { |
|
if (Object.keys(localization).length) return; |
|
if (Object.keys(opts).length === 0) return; |
|
|
|
let _nodesCount = 0, _now = performance.now() |
|
|
|
for (const mutation of mutations) { |
|
if (mutation.type === 'characterData') { |
|
if (mutation.target?.parentElement?.parentElement?.tagName === 'LABEL') { |
|
translateEl(mutation.target) |
|
} |
|
} else if (mutation.type === 'attributes') { |
|
_nodesCount++ |
|
translateEl(mutation.target) |
|
} else { |
|
mutation.addedNodes.forEach(node => { |
|
if (node.className === 'bilingual__trans_wrapper') return |
|
|
|
_nodesCount++ |
|
if (node.nodeType === 1 && /(output|gradio)-(html|markdown)/.test(node.className)) { |
|
translateEl(node, { rich: true }) |
|
} else if (node.nodeType === 3) { |
|
doTranslate(node, node.textContent, 'text') |
|
} else { |
|
translateEl(node, { deep: true }) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
if (_nodesCount > 0) { |
|
logger.info(`UI Update #${_count++}: ${performance.now() - _now} ms, ${_nodesCount} nodes`, mutations) |
|
} |
|
|
|
if (loaded) return; |
|
if (i18n) return; |
|
|
|
loaded = true |
|
setup() |
|
}) |
|
|
|
observer.observe(gradioApp(), { |
|
characterData: true, |
|
childList: true, |
|
subtree: true, |
|
attributes: true, |
|
attributeFilter: ['title', 'placeholder'] |
|
}) |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init) |
|
})(); |
|
|