sd-webui-bilingual-localiza.../javascript/bilingual_localization.js

259 lines
7.3 KiB
JavaScript

(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;
}
fieldset span.text-gray-500:has(.bilingual__trans_wrapper),
.gr-block.gr-box span.text-gray-500:has(.bilingual__trans_wrapper),
label.block span:has(.bilingual__trans_wrapper) {
top: -0.8em;
line-height: 1;
}
#txtimg_hr_finalres .bilingual__trans_wrapper em,
#tab_ti .output-html .bilingual__trans_wrapper em,
#available_extensions .extension-tag .bilingual__trans_wrapper em {
display: none;
}
#settings .bilingual__trans_wrapper:not(#settings .tabitem .bilingual__trans_wrapper),
label>span>.bilingual__trans_wrapper,
.w-full>span>.bilingual__trans_wrapper,
.output-html .bilingual__trans_wrapper:not(th .bilingual__trans_wrapper) {
font-size: 12px;
align-items: flex-start;
}
#extensions label .bilingual__trans_wrapper,
#available_extensions td .bilingual__trans_wrapper {
font-size: inherit;
line-height: inherit;
}
textarea::placeholder {
line-height: 1;
padding: 4px 0;
}
label>span {
line-height: 1;
}
div[data-testid="image"]>div>div.touch-none>div {
background-color: rgba(255, 255, 255, .6);
color: #222;
}`
let i18n = null, config = null;
// First load
function setup() {
config = {
enabled: opts["bilingual_localization_enabled"],
file: opts["bilingual_localization_file"],
dirs: opts["bilingual_localization_dirs"],
order: opts["bilingual_localization_order"]
}
let { enabled, file, dirs } = config
if (!enabled || file === "None" || dirs === "None") return
dirs = JSON.parse(dirs)
// Load localization file
i18n = JSON.parse(readFile(dirs[file]))
translatePage()
}
// Translate page
function translatePage() {
if (!i18n) return
querySelectorAll([
"label span, fieldset span, button", // major label and button description text
"textarea[placeholder], select[title], option", // text box placeholder and select element
".transition > div > span:not([class])", // collapse panel added by extension
".tabitem .pointer-events-none", // upper left corner of image upload panel
".output-html:not(#footer)", // output html exclude footer
"#lightboxModal span" // image preview lightbox
])
.forEach(el => translateEl(el, { deep: true }))
querySelectorAll([
'div[data-testid="image"] > div > div', // description of image upload panel
'#extras_image_batch > div', // description of extras image batch file upload panel
])
.forEach(el => translateEl(el, { rich: true }))
}
const ignore_selector = [
'.bilingual__trans_wrapper', // self
'.resultsFlexContainer', // tag-autocomplete
'#setting_sd_model_checkpoint select', // model checkpoint
'#setting_sd_vae select', // vae model
'#txt2img_styles, #img2txt_styles', // styles select
'.extra-network-cards .card .actions .name', // extra network cards name
'script, style, svg, g, path', // script / style / svg elements
]
// Translate element
function translateEl(el, { deep = false, rich = false } = {}) {
if (!i18n) return // translation not ready.
if (el.matches?.(ignore_selector)) return // ignore some elements.
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')
} else {
doTranslate(el, el.textContent, 'element')
}
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 })
}
})
}
const re_num = /^[\.\d]+$/,
re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
function doTranslate(el, source, type) {
if (el.__bilingual_localization_translated) return
source = source.trim()
if (!source) return
if (re_num.test(source)) return
if (re_emoji.test(source)) return
let translation = i18n[source]
if (!translation) return
if (source === translation) 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)
textNode && textNode.replaceWith(htmlEl)
} else {
el.replaceWith(htmlEl)
}
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;
}
Object.defineProperty(el, '__bilingual_localization_translated', { value: true })
}
function querySelector(...args) {
return gradioApp()?.querySelector(...args)
}
function querySelectorAll(...args) {
return gradioApp()?.querySelectorAll(...args)
}
function parseHtmlStringToElement(htmlStr) {
const template = document.createElement('template')
template.insertAdjacentHTML('afterbegin', htmlStr)
return template.firstElementChild
}
function htmlEncode(htmlStr) {
return htmlStr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
}
// Load file
function readFile(filePath) {
let request = new XMLHttpRequest();
request.open("GET", `file=${filePath}`, false);
request.send(null);
return request.responseText;
}
function init() {
// Add style to dom
let $styleEL = document.createElement('style');
if ($styleEL.styleSheet) {
$styleEL.styleSheet.cssText = customCSS;
} else {
$styleEL.appendChild(document.createTextNode(customCSS));
}
gradioApp().appendChild($styleEL);
let loaded = false
onUiUpdate((m) => {
if (Object.keys(localization).length) return; // disabled if original translation enabled
if (Object.keys(opts).length === 0) return; // does nothing if opts is not loaded
m.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.className === 'output-html') {
translateEl(node, { rich: true })
} else {
translateEl(node, { deep: true })
}
})
})
if (loaded) return;
if (i18n) return;
loaded = true
setup()
})
}
// Init after page loaded
document.addEventListener('DOMContentLoaded', init)
})();