feat: Refactoring the extension to support select localization file
parent
022b17f105
commit
7f10aa3636
|
|
@ -0,0 +1 @@
|
||||||
|
__pycache__/
|
||||||
2105
i18n/i18n.json
2105
i18n/i18n.json
File diff suppressed because it is too large
Load Diff
5700
i18n/tags_i18n.csv
5700
i18n/tags_i18n.csv
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,200 @@
|
||||||
|
(function () {
|
||||||
|
const customCSS = `
|
||||||
|
.bilingual__trans_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bilingual__trans_wrapper em {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_ui__prompt_trans {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings .bilingual__trans_wrapper,
|
||||||
|
label>span>.bilingual__trans_wrapper,
|
||||||
|
.w-full>span>.bilingual__trans_wrapper {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::placeholder {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 5px 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;
|
||||||
|
|
||||||
|
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]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate page
|
||||||
|
function translatePage() {
|
||||||
|
if (!i18n) return
|
||||||
|
|
||||||
|
querySelectorAll(`
|
||||||
|
label span, fieldset span, thead th, th span, button,
|
||||||
|
textarea[placeholder], select[title], option,
|
||||||
|
.transition > div > span:not([class]),
|
||||||
|
.tabitem .pointer-events-none,
|
||||||
|
.output-html, #lightboxModal span
|
||||||
|
`)
|
||||||
|
.forEach(el => translateEl(el))
|
||||||
|
|
||||||
|
querySelectorAll(`
|
||||||
|
.output-html:not(#footer) p,
|
||||||
|
.output-html tabel:not(#extensions),
|
||||||
|
div[data-testid="image"] > div > div,
|
||||||
|
#extras_image_batch > div,
|
||||||
|
.extension-tag
|
||||||
|
`)
|
||||||
|
.forEach(el => translateEl(el, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate element
|
||||||
|
function translateEl(el, deep) {
|
||||||
|
if (el.className === 'bilingual__trans_wrapper') return
|
||||||
|
|
||||||
|
if (deep) {
|
||||||
|
Array.from(el.childNodes).forEach(node => {
|
||||||
|
if (node.nodeName === '#text') {
|
||||||
|
doTranslate(node, node.textContent, 'node')
|
||||||
|
} else if (node.childNodes.length > 0) {
|
||||||
|
translateEl(node, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.textContent && el.tagName !== 'SELECT') {
|
||||||
|
if (el.tagName === 'OPTION') {
|
||||||
|
doTranslate(el, el.textContent, 'option')
|
||||||
|
} else {
|
||||||
|
doTranslate(el, el.textContent, 'element')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.title) {
|
||||||
|
doTranslate(el, el.title, 'title')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.placeholder) {
|
||||||
|
doTranslate(el, el.placeholder, 'placeholder')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doTranslate(el, source, type) {
|
||||||
|
source = source.trim()
|
||||||
|
if (!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 'node':
|
||||||
|
el.textContent = translation
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'element':
|
||||||
|
if (el.childNodes.length === 1 || !el.classList.contains('pointer-events-none')) {
|
||||||
|
el.innerHTML = `<div class="bilingual__trans_wrapper"><em>${translation}</em>${source}</div>`
|
||||||
|
} else {
|
||||||
|
const textNode = Array.from(el.childNodes).find(node => node.nodeName === '#text' && node.textContent.trim() === source)
|
||||||
|
|
||||||
|
textNode && (textNode.textContent = `${translation} (${source})`)
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function querySelector(...args) {
|
||||||
|
return gradioApp()?.querySelector(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
function querySelectorAll(...args) {
|
||||||
|
return gradioApp()?.querySelectorAll(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
translatePage()
|
||||||
|
|
||||||
|
if (loaded) return
|
||||||
|
if (Object.keys(localization).length) return
|
||||||
|
if (Object.keys(opts).length === 0) return;
|
||||||
|
if (i18n) return;
|
||||||
|
|
||||||
|
loaded = true
|
||||||
|
setup()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init after page loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', init)
|
||||||
|
})();
|
||||||
|
|
@ -1,377 +0,0 @@
|
||||||
(function () {
|
|
||||||
const EXT_BASE = 'extensions/sd-webui-zh_cn'
|
|
||||||
const customCSS = `
|
|
||||||
.custom_ui__trans_wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom_ui__trans_wrapper em {
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom_ui__prompt_trans {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings .custom_ui__trans_wrapper,
|
|
||||||
label>span>.custom_ui__trans_wrapper,
|
|
||||||
.w-full>span>.custom_ui__trans_wrapper {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea::placeholder {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
label>span {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[data-testid="image"]>div>div.touch-none>div {
|
|
||||||
background-color: rgba(255, 255, 255, .6);
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
#txt2img_generate, #img2img_generate {
|
|
||||||
scroll-margin-bottom: 50px;
|
|
||||||
}`
|
|
||||||
|
|
||||||
let labelMap = {} // 提示标签->id引用映射
|
|
||||||
// 构造标签引用映射
|
|
||||||
function buildLabelReferenceMap(tab = 'tab_txt2img') {
|
|
||||||
return [...querySelectorAll(`#${tab} label[for] span`)].reduce((labelMap, span) => {
|
|
||||||
const label = span.parentNode,
|
|
||||||
key = span.textContent.trim(),
|
|
||||||
val = label.getAttribute('for')
|
|
||||||
|
|
||||||
labelMap[key] = val
|
|
||||||
|
|
||||||
return labelMap
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载本地化语料
|
|
||||||
const i18n = JSON.parse(readFile(`${EXT_BASE}/i18n/i18n.json`))
|
|
||||||
|
|
||||||
// 翻译页面
|
|
||||||
function translatePage() {
|
|
||||||
// const selector = ['tab_txt2img', 'tab_img2img', 'tab_extras', 'tab_pnginfo'].map(tab => `#${tab} label span, #${tab} button`).join(',')
|
|
||||||
[...querySelectorAll('label span, fieldset span, th span, button, textarea[placeholder], select[title], option, div.transition > div > span:not([class]), .tabitem .pointer-events-none, #lightboxModal span, thead th, .output-html')].forEach(el => translateEl(el));
|
|
||||||
[...querySelectorAll('.output-html:not(#footer) p, .output-html tabel:not(#extensions), div[data-testid="image"] > div > div, #extras_image_batch > div, .extension-tag')].forEach(el => translateEl(el, 'node'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 翻译元素
|
|
||||||
function translateEl(el, type) {
|
|
||||||
if (el.className === 'custom_ui__trans_wrapper') return
|
|
||||||
|
|
||||||
if (type === 'node') {
|
|
||||||
Array.from(el.childNodes).forEach(node => {
|
|
||||||
if (node.nodeName === '#text') {
|
|
||||||
doTranslate(node, node.textContent, 'node')
|
|
||||||
} else if (node.childNodes.length > 0) {
|
|
||||||
translateEl(node, 'node')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.textContent && el.tagName !== 'SELECT') {
|
|
||||||
if (el.tagName === 'OPTION') {
|
|
||||||
doTranslate(el, el.textContent, 'option')
|
|
||||||
} else {
|
|
||||||
doTranslate(el, el.textContent, 'element')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.title) {
|
|
||||||
doTranslate(el, el.title, 'title')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.placeholder) {
|
|
||||||
doTranslate(el, el.placeholder, 'placeholder')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doTranslate(el, source, type) {
|
|
||||||
source = source.trim()
|
|
||||||
|
|
||||||
if (!source) return
|
|
||||||
|
|
||||||
const translate = i18n[source]
|
|
||||||
|
|
||||||
if (!translate) return
|
|
||||||
if (source === translate) return
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'node':
|
|
||||||
el.textContent = translate
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'element':
|
|
||||||
if (el.childNodes.length === 1 || !el.classList.contains('pointer-events-none')) {
|
|
||||||
el.innerHTML = `<div class="custom_ui__trans_wrapper"><em>${translate}</em>${source}</div>`
|
|
||||||
} else {
|
|
||||||
const textNode = Array.from(el.childNodes).find(node => node.nodeName === '#text' && node.textContent.trim() === source)
|
|
||||||
if (textNode) {
|
|
||||||
textNode.textContent = `${translate} (${source})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'option':
|
|
||||||
el.textContent = `${translate} (${source})`
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'title':
|
|
||||||
el.title = `${translate}\n${source}`
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'placeholder':
|
|
||||||
el.placeholder = `${translate}\n\n${source}`
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tagsI18n = null
|
|
||||||
// 翻译标签
|
|
||||||
function translatePrompt(text) {
|
|
||||||
if (!tagsI18n) {
|
|
||||||
let _tmp_list = loadCSV(`${EXT_BASE}/i18n/tags_i18n.csv`)
|
|
||||||
tagsI18n = _tmp_list.reduce((tagsI18n, [tag, _, i18n]) => {
|
|
||||||
if (!tag || !i18n || tag === i18n) return tagsI18n // 过滤无效数据
|
|
||||||
|
|
||||||
tagsI18n[tag] = i18n
|
|
||||||
return tagsI18n
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 转小写并去除多余空格和权重字符,保留转义括号
|
|
||||||
'{{masterpiece}}, (best quality:1.215), [lowkey:0.03], keyboard \\(computer\\)'
|
|
||||||
=> 'masterpiece, best quality, lowkey, keyboard (computer)'
|
|
||||||
*/
|
|
||||||
let index = 0
|
|
||||||
let promptText = text
|
|
||||||
.toLowerCase() // 转小写
|
|
||||||
.replace(/\s\s+/g, ' ') // 去除多余空格
|
|
||||||
.replace(/:(\d*\.?\d*)/g, '') // 去除权重值
|
|
||||||
.replace(/\\[(){}\[\]]/g, '\uFFFF') // 暂存转义括号为特殊字符
|
|
||||||
.replace(/[(){}\[\]]/g, '') // 去除其余所有括号
|
|
||||||
.replace(/\uFFFF/g, () => ['(', ')'][index++ % 2]) // 还原转义括号
|
|
||||||
|
|
||||||
// 按逗号分割匹配翻译
|
|
||||||
const promptList = promptText
|
|
||||||
.split(/,|,/)
|
|
||||||
.map(tag => {
|
|
||||||
tag = tag.trim()
|
|
||||||
let parsed_tag = tag.replace(/\s+/g, '_')
|
|
||||||
return tagsI18n[parsed_tag] || tag
|
|
||||||
})
|
|
||||||
|
|
||||||
const promptTrans = promptList.join(', ')
|
|
||||||
|
|
||||||
return promptTrans
|
|
||||||
}
|
|
||||||
|
|
||||||
// 节流函数
|
|
||||||
function throttle(fn, delay) {
|
|
||||||
let timer = null
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
if (timer) return
|
|
||||||
|
|
||||||
fn.apply(this, arguments)
|
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
timer = null
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防抖函数
|
|
||||||
function debounce(fn, delay) {
|
|
||||||
let ts = 0, timer = null
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
if (Date.now() - ts < delay) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
fn.apply(this, arguments)
|
|
||||||
}, delay)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timer)
|
|
||||||
fn.apply(this, arguments)
|
|
||||||
|
|
||||||
ts = Date.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function querySelector(...args) {
|
|
||||||
return gradioApp()?.querySelector(...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
function querySelectorAll(...args) {
|
|
||||||
return gradioApp()?.querySelectorAll(...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过标签选择对应配置项元素
|
|
||||||
function querySelectorByLabel(label, tab = 'tab_txt2img') {
|
|
||||||
const id = labelMap[tab][label]
|
|
||||||
|
|
||||||
if (!id) return
|
|
||||||
|
|
||||||
return querySelector(`#${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过选择器模拟触发修改值
|
|
||||||
function changeVal(selector, value) {
|
|
||||||
const el = selector instanceof Element ? selector : querySelector(selector)
|
|
||||||
el.value = value
|
|
||||||
|
|
||||||
const event = new Event('input', { bubbles: true })
|
|
||||||
Object.defineProperty(event, 'target', { writable: false, value: el });
|
|
||||||
el.dispatchEvent(event)
|
|
||||||
|
|
||||||
el.dispatchEvent(new Event('focusout')) // 兼容tagAutocomplete插件
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过标签修改值
|
|
||||||
function changeValByLabel(label, value, tab = 'tab_txt2img') {
|
|
||||||
const _gradio_conf = gradio_config.components.find(_ => _.props.label === label)
|
|
||||||
if (_gradio_conf) _gradio_conf.props.value = value
|
|
||||||
|
|
||||||
const id = labelMap[tab][label]
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
if (_gradio_conf) {
|
|
||||||
const { elem_id } = _gradio_conf.props
|
|
||||||
const _el = querySelector(`#${elem_id}`).querySelector('input, textarea, select')
|
|
||||||
_el && changeVal(_el, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
changeVal(`#${id}`, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load file
|
|
||||||
function readFile(filePath) {
|
|
||||||
let request = new XMLHttpRequest();
|
|
||||||
request.open("GET", `file=${filePath}`, false);
|
|
||||||
request.send(null);
|
|
||||||
return request.responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load CSV
|
|
||||||
function loadCSV(path) {
|
|
||||||
let text = readFile(path);
|
|
||||||
return parseCSV(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
|
|
||||||
function parseCSV(str) {
|
|
||||||
var arr = [];
|
|
||||||
var quote = false; // 'true' means we're inside a quoted field
|
|
||||||
|
|
||||||
// Iterate over each character, keep track of current row and column (of the returned array)
|
|
||||||
for (var row = 0, col = 0, c = 0; c < str.length; c++) {
|
|
||||||
var cc = str[c], nc = str[c + 1]; // Current character, next character
|
|
||||||
arr[row] = arr[row] || []; // Create a new row if necessary
|
|
||||||
arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
|
|
||||||
|
|
||||||
// If the current character is a quotation mark, and we're inside a
|
|
||||||
// quoted field, and the next character is also a quotation mark,
|
|
||||||
// add a quotation mark to the current column and skip the next character
|
|
||||||
if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
|
|
||||||
|
|
||||||
// If it's just one quotation mark, begin/end quoted field
|
|
||||||
if (cc == '"') { quote = !quote; continue; }
|
|
||||||
|
|
||||||
// If it's a comma and we're not in a quoted field, move on to the next column
|
|
||||||
if (cc == ',' && !quote) { ++col; continue; }
|
|
||||||
|
|
||||||
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
|
|
||||||
// and move on to the next row and move to column 0 of that new row
|
|
||||||
if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
|
|
||||||
|
|
||||||
// If it's a newline (LF or CR) and we're not in a quoted field,
|
|
||||||
// move on to the next row and move to column 0 of that new row
|
|
||||||
if (cc == '\n' && !quote) { ++row; col = 0; continue; }
|
|
||||||
if (cc == '\r' && !quote) { ++row; col = 0; continue; }
|
|
||||||
|
|
||||||
// Otherwise, append the current character to the current column
|
|
||||||
arr[row][col] += cc;
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加自定义UI
|
|
||||||
function add_custom_ui() {
|
|
||||||
// 未获取到gradio-app重试
|
|
||||||
if (!document.getElementsByTagName('gradio-app').length) {
|
|
||||||
requestAnimationFrame(add_custom_ui)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防止重复添加
|
|
||||||
if (querySelector('#custom_preset')) return
|
|
||||||
|
|
||||||
// 未获取到注入点dom重试
|
|
||||||
const $txt2imgTabEl = querySelector('#tab_txt2img')
|
|
||||||
if (!$txt2imgTabEl) {
|
|
||||||
requestAnimationFrame(add_custom_ui)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 构造标签引用映射
|
|
||||||
labelMap = {
|
|
||||||
tab_txt2img: buildLabelReferenceMap('tab_txt2img'),
|
|
||||||
tab_img2img: buildLabelReferenceMap('tab_img2img'),
|
|
||||||
tab_extras: buildLabelReferenceMap('tab_extras'),
|
|
||||||
tab_pnginfo: buildLabelReferenceMap('tab_pnginfo'),
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI更新时翻译页面
|
|
||||||
onUiUpdate(translatePage)
|
|
||||||
|
|
||||||
// 添加prompt翻译框
|
|
||||||
querySelectorAll('#txt2img_prompt textarea, #img2img_prompt textarea').forEach(el => {
|
|
||||||
const $wrapper = document.createElement('div')
|
|
||||||
$wrapper.className = 'custom_ui__prompt_trans text-sm text-gray-400'
|
|
||||||
|
|
||||||
// 内容变动时执行翻译
|
|
||||||
el.addEventListener('input', debounce((e) => {
|
|
||||||
const text = el.value.trim()
|
|
||||||
|
|
||||||
const promptTrans = translatePrompt(text)
|
|
||||||
|
|
||||||
$wrapper.textContent = promptTrans
|
|
||||||
}, 500))
|
|
||||||
|
|
||||||
el.insertAdjacentElement('afterend', $wrapper)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
add_custom_ui()
|
|
||||||
})();
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# This helper script loads the list of localization files and
|
||||||
|
# exposes the current localization file name and path to the javascript side
|
||||||
|
|
||||||
|
import gradio as gr
|
||||||
|
from pathlib import Path
|
||||||
|
from modules import localization, script_callbacks, shared
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Webui root path
|
||||||
|
ROOT_DIR = Path().absolute()
|
||||||
|
|
||||||
|
# The localization files
|
||||||
|
I18N_DIRS = { k: str(Path(v).relative_to(ROOT_DIR).as_posix()) for k, v in localization.localizations.items() }
|
||||||
|
|
||||||
|
# Register extension options
|
||||||
|
def on_ui_settings():
|
||||||
|
BL_SECTION = ("bl", "Bilingual Localization")
|
||||||
|
# enable in settings
|
||||||
|
shared.opts.add_option("bilingual_localization_enabled", shared.OptionInfo(True, "Enable Bilingual Localization", section=BL_SECTION))
|
||||||
|
|
||||||
|
# current localization file
|
||||||
|
shared.opts.add_option("bilingual_localization_file", shared.OptionInfo("None", "Localization file (Please leave `User interface` - `Localization` as None)", gr.Dropdown, lambda: {"choices": ["None"] + list(localization.localizations.keys())}, refresh=lambda: localization.list_localizations(shared.cmd_opts.localizations_dir), section=BL_SECTION))
|
||||||
|
|
||||||
|
# translation order
|
||||||
|
shared.opts.add_option("bilingual_localization_order", shared.OptionInfo("Translation First", "Translation display order", gr.Radio, {"choices": ["Translation First", "Original First"]}, section=BL_SECTION))
|
||||||
|
|
||||||
|
# all localization files path in hidden option
|
||||||
|
shared.opts.add_option("bilingual_localization_dirs", shared.OptionInfo(json.dumps(I18N_DIRS), "Localization dirs", section=BL_SECTION, component_args={"visible": False}))
|
||||||
|
|
||||||
|
script_callbacks.on_ui_settings(on_ui_settings)
|
||||||
Loading…
Reference in New Issue