redo all locales

Signed-off-by: vladmandic <mandic00@live.com>
pull/4653/head
vladmandic 2026-02-14 21:48:06 +01:00
parent 6876d2b84d
commit 73b90c5228
37 changed files with 264226 additions and 78493 deletions

View File

@ -1,7 +1,15 @@
# Change Log for SD.Next
## Update for 2026-02-12
## Update for 2026-02-14
### Highlights for 2026-02-14
TBD
### Details for 2026-02-14
- **Models**
- TBD
- **Image manipulation**
- use high-quality [sharpfin](https://github.com/drhead/Sharpfin) accelerated library
when available (cuda-only), thanks @CalamitousFelicitousness
@ -18,6 +26,11 @@
instead of being used implicitly via quantization, thanks @CalamitousFelicitousness
- removed: old `codeformer` and `gfpgan` face restorers, thanks @CalamitousFelicitousness
- **UI**
- ui: **localization** improved translation quality and new translations locales:
*en, en1, en2, en3, en4, hr, es, it, fr, de, pt, ru, zh, ja, ko, hi, ar, bn, ur, id, vi, tr, sr, po, he, xx, yy, qq, tlh*
yes, this now includes stuff like *latin, esperanto, arabic, hebrew, klingon* and a lot more!
and also intruce some pseudo-locales such as: *techno-babbel*, *for-n00bs*
*hint*: click on locale icon in bottom-left corner to cycle through available locales, or set default in *settings -> ui*
- ui: **themes** add *CTD-NT64Light*, *CTD-NT64Medium* and *CTD-NT64Dark*, thanks @resonantsky
- ui: **gallery** add option to auto-refresh gallery, thanks @awsr
- **Internal**

View File

@ -129,6 +129,7 @@ const jsConfig = defineConfig([
camelcase: 'off',
'default-case': 'off',
'max-classes-per-file': 'warn',
'guard-for-in': 'off',
'no-await-in-loop': 'off',
'no-bitwise': 'off',
'no-continue': 'off',

@ -1 +1 @@
Subproject commit ddf821483c8bcdac4868f15a9a838b45b4dd30ad
Subproject commit 264ac9f38a718b37ca2c6ce17fd906d5d153cb2e

@ -1 +1 @@
Subproject commit d25f3ee3a012d8a99cd6ded09152126a877b742e
Subproject commit fc7cf10dcc3f17377b6a18c4cd0dbd2be5480f0b

11074
html/locale_ar.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_bn.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11074
html/locale_he.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_hi.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11074
html/locale_id.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11074
html/locale_nb.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_po.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11074
html/locale_qq.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11074
html/locale_sr.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_tb.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_tlh.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_tr.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_ur.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_vi.json Normal file

File diff suppressed because it is too large Load Diff

11074
html/locale_xx.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -688,23 +688,23 @@ def check_diffusers():
t_start = time.time()
if args.skip_all:
return
sha = '5bf248ddd8796b4f4958559429071a28f9b2dd3a' # diffusers commit hash
target_commit = '6141ae2348c1af14836ac076bf089a0259daa340' # diffusers commit hash
# if args.use_rocm or args.use_zluda or args.use_directml:
# sha = '043ab2520f6a19fce78e6e060a68dbc947edb9f9' # lock diffusers versions for now
pkg = pkg_resources.working_set.by_key.get('diffusers', None)
minor = int(pkg.version.split('.')[1] if pkg is not None else -1)
cur = opts.get('diffusers_version', '') if minor > -1 else ''
if (minor == -1) or ((cur != sha) and (not args.experimental)):
current = opts.get('diffusers_version', '') if minor > -1 else ''
if (minor == -1) or ((current != target_commit) and (not args.experimental)):
if minor == -1:
log.info(f'Diffusers install: commit={sha}')
log.info(f'Diffusers install: commit={target_commit}')
else:
log.info(f'Diffusers update: current={pkg.version} hash={cur} target={sha}')
log.info(f'Diffusers update: current={pkg.version} hash={current} target={target_commit}')
pip('uninstall --yes diffusers', ignore=True, quiet=True, uv=False)
if args.skip_git:
log.warning('Git: marked as not available but required for diffusers installation')
pip(f'install --upgrade git+https://github.com/huggingface/diffusers@{sha}', ignore=False, quiet=True, uv=False)
pip(f'install --upgrade git+https://github.com/huggingface/diffusers@{target_commit}', ignore=False, quiet=True, uv=False)
global diffusers_commit # pylint: disable=global-statement
diffusers_commit = sha
diffusers_commit = target_commit
ts('diffusers', t_start)

View File

@ -1,4 +1,4 @@
const allLocales = ['en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'pt', 'hr', 'ru', 'zh'];
const allLocales = ['en', 'tb', 'nb', 'hr', 'es', 'it', 'fr', 'de', 'pt', 'ru', 'zh', 'ja', 'ko', 'hi', 'ar', 'bn', 'ur', 'id', 'vi', 'tr', 'sr', 'po', 'he', 'xx', 'qq', 'tlh'];
const localeData = {
prev: null,
locale: null,
@ -168,48 +168,6 @@ async function tooltipHide(e) {
localeData.currentElement = null;
}
async function validateHints(json, elements, tab) {
json.missing = [];
const data = Object.values(json).flat().filter((e) => e.hint.length > 0);
for (const e of data) e.label = e.label.trim();
if (tab) {
elements = elements.filter((el) => el.closest(`#${tab}_tabitem`)); // include only elements within specified tab
elements = elements.filter((el) => !el.closest(`#${tab}_scripts_tabitem`));
}
let original = elements.map((e) => e.textContent.toLowerCase().trim()).sort(); // should be case sensitive
let duplicateUI = original.filter((e, i, a) => a.indexOf(e.toLowerCase()) !== i).sort();
original = [...new Set(original)]; // remove duplicates
duplicateUI = [...new Set(duplicateUI)]; // remove duplicates
const current = data.map((e) => e.label.toLowerCase().trim()).sort(); // should be case sensitive
// log('all elements:', original);
// log('all hints:', current);
log('hints-differences', { elements: original.length, hints: current.length });
const missingHints = original.filter((e) => !current.includes(e.toLowerCase())).sort();
const orphanedHints = current.filter((e) => !original.includes(e.toLowerCase())).sort();
const duplicateHints = current.filter((e, i, a) => a.indexOf(e.toLowerCase()) !== i).sort();
log('duplicate hints:', duplicateHints);
log('duplicate labels:', duplicateUI);
return [missingHints, orphanedHints];
}
async function addMissingHints(json, missingHints) {
if (missingHints.length === 0) return;
json.missing = [];
for (const h of missingHints.sort()) {
if (h.length <= 1) continue;
json.missing.push({ id: '', label: h, localized: '', hint: h, longHint: '' }); // Add longHint property
}
log('missing hints', missingHints);
log('added missing hints:', { missing: json.missing });
}
async function removeOrphanedHints(json, orphanedHints) {
const data = Object.values(json).flat().filter((e) => e.hint.length > 0);
for (const e of data) e.label = e.label.trim();
const orphaned = data.filter((e) => orphanedHints.includes(e.label.toLowerCase()));
log('orphaned hints:', { orphaned });
}
async function replaceButtonText(el) {
// https://www.nerdfonts.com/cheat-sheet
// use unicode of icon with format nf-md-<icon>_circle
@ -289,7 +247,58 @@ async function setHint(el, entry) {
}
}
async function setHints(analyze = false) {
function createLocaleJSON() {
const excludeText = ['▼']; // add any common non-label elements to exclude
const ecxcludeIds = ['logo_nav']; // add any specific element IDs to exclude
const elements = [
...Array.from(gradioApp().querySelectorAll('button')),
...Array.from(gradioApp().querySelectorAll('h1')),
...Array.from(gradioApp().querySelectorAll('h2')),
...Array.from(gradioApp().querySelectorAll('h3')),
...Array.from(gradioApp().querySelectorAll('label > span')),
...Array.from(gradioApp().querySelectorAll('.label-wrap > span')),
];
const json = {};
const allSeen = {};
for (const el of elements) {
const label = el.textContent.trim();
if (!label || label.length < 1 || label.length > 1024) continue; // likely not UI element
if (excludeText.includes(label)) continue; // skip common non-label elements
if (ecxcludeIds.includes(el.id)) continue; // skip specific element IDs
let hint = el.dataset.hint || '';
if (hint.toLowerCase() === label.toLowerCase()) hint = ''; // skip if hint is same as label
if (Object.keys(allSeen).includes(label.toLowerCase())) {
if (hint.length === 0 || allSeen[label.toLowerCase()] === hint) continue; // seen this label and hint is empty or same as before
hint = allSeen[label.toLowerCase()]; // use existing hint for this label
}
allSeen[label.toLowerCase()] = hint; // track seen labels
let section = label[0].toLowerCase();
if (section >= '0' && section <= '9') section = '0';
if ((section < 'a' || section > 'z') && section !== '0') section = '_';
let ui = el.closest('.group-extension')
|| el.closest('.group-scripts')
|| el.closest('.main-tab')
|| el.closest('.settings_section')
|| el.closest('.tabitem')
|| 'other';
ui = ui?.id?.replace('_tabitem_parent', '').replace('_section_row', '');
if (ui?.includes('_script')) ui = ui?.split('_').slice(1).join('_').split(':')[0];
if (!json[section]) json[section] = [];
const entry = { id: el.id || '', label, localized: '', hint, ui };
json[section].push(entry); // add new entry
}
const sorted = Object.keys(json).sort().reduce((obj, key) => {
obj[key] = json[key];
return obj;
}, {});
console.log('localeJSON', sorted);
}
async function setHints() {
let json = {};
let overrideData = [];
if (localeData.finished) return;
@ -311,6 +320,7 @@ async function setHints(analyze = false) {
let localized = 0;
let hints = 0;
const t0 = performance.now();
for (const el of elements) {
// localize elements text
let found;
@ -318,8 +328,8 @@ async function setHints(analyze = false) {
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
if (found?.localized?.length > 0) {
if (!el.dataset.original) el.dataset.original = el.textContent;
localized++;
replaceTextContent(el, found.localized);
localized++;
} else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { // reset to english
replaceTextContent(el, found.label);
}
@ -336,20 +346,8 @@ async function setHints(analyze = false) {
log('touchDevice', isTouchDevice);
log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, override: overrideData.length, time: Math.round(t1 - t0) });
// sortUIElements();
if (analyze) {
log('analyzing hints', 'control_tabitem');
const [missingHints, orphanedHints] = await validateHints(json, elements);
await addMissingHints(json, missingHints);
await removeOrphanedHints(json, orphanedHints);
}
}
const analyzeHints = async () => {
localeData.finished = false;
localeData.data = [];
await setHints(true);
};
// Apply hints to a single element immediately
async function applyHintToElement(el) {
if (!localeData.data || localeData.data.length === 0) return;

View File

@ -255,10 +255,9 @@ def check_quant(module: str = ''):
def check_nunchaku(module: str = ''):
from modules import shared
model_name = getattr(shared.opts, 'sd_model_checkpoint', '')
if '+nunchaku' not in model_name:
if 'nunchaku' not in shared.opts.sd_model_checkpoint.lower():
return False
base_path = model_name.split('+')[0]
base_path = shared.opts.sd_model_checkpoint.split('+')[0]
for v in shared.reference_models.values():
if v.get('path', '') != base_path:
continue

View File

@ -44,10 +44,10 @@
"eslint": "^9.39.2",
"eslint-config-airbnb-extended": "^3.0.0",
"eslint-plugin-promise": "^7.2.1",
"@google/genai": "^1.41.0",
"globals": "^17.0.0"
},
"dependencies": {
"@google/generative-ai": "^0.24.1",
"argparse": "^2.0.1"
},
"//": {

View File

@ -1,67 +0,0 @@
#!/usr/bin/env node
// script used to localize sdnext ui and hints to multiple languages using google gemini ai
const fs = require('node:fs');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const api_key = process.env.GOOGLE_AI_API_KEY;
const model = 'gemini-2.5-flash';
const prompt = `Translate attached JSON from English to {language} using following rules: fields id, label and reload should be preserved from original, field localized should be a translated version of field label and field hint should be translated in-place.
if field is less than 3 characters, do not translate it and keep it as is.
Every JSON entry should have id, label, localized, reload and hint fields.
Output should be pure JSON without any additional text. To better match translation, context of the text is related to Stable Diffusion and topic of Generative AI.`;
const languages = {
hr: 'Croatian',
de: 'German',
es: 'Spanish',
fr: 'French',
it: 'Italian',
pt: 'Portuguese',
zh: 'Chinese',
ja: 'Japanese',
ko: 'Korean',
ru: 'Russian',
};
const chunkLines = 100;
async function localize() {
if (!api_key || api_key.length < 10) {
console.error('localize: set GOOGLE_AI_API_KEY env variable with your API key');
process.exit();
}
const genAI = new GoogleGenerativeAI(api_key);
const instance = genAI.getGenerativeModel({ model });
const raw = fs.readFileSync('html/locale_en.json');
const json = JSON.parse(raw);
for (const locale of Object.keys(languages)) {
const lang = languages[locale];
const target = prompt.replace('{language}', lang).trim();
const output = {};
const fn = `html/locale_${locale}.json`;
for (const section of Object.keys(json)) {
const data = json[section];
output[section] = [];
for (let i = 0; i < data.length; i += chunkLines) {
let markdown;
try {
const chunk = data.slice(i, i + chunkLines);
const result = await instance.generateContent([target, JSON.stringify(chunk)]);
markdown = result.response.text();
const text = markdown.replaceAll('```', '').replace(/^.*\n/, '');
const parsed = JSON.parse(text);
output[section].push(...parsed);
console.log(`localize: locale=${locale} lang=${lang} section=${section} chunk=${chunk.length} output=${output[section].length} fn=${fn}`);
} catch (err) {
console.error('localize:', err);
console.error('localize input:', { target, section, i });
console.error('localize output:', { markdown });
}
}
const txt = JSON.stringify(output, null, 2);
fs.writeFileSync(fn, txt);
}
}
}
localize();

150
test/localize.mjs Normal file
View File

@ -0,0 +1,150 @@
#!/usr/bin/env node
// script used to localize sdnext ui and hints to multiple languages using google gemini ai
import { GoogleGenAI, Type, ThinkingLevel } from '@google/genai';
import * as fs from 'node:fs';
import * as process from 'node:process';
const apiKey = process.env.GOOGLE_API_KEY;
const model = 'gemini-3-flash-preview';
const prompt = `## You are expert translator AI.
Translate attached JSON from English to {language}.
## Translation Rules:
- Fields \`id\`, \`label\` and \`reload\` should be preserved from original.
- Field \`localized\` should be a translated version of field \`label\`.
- If field \`localized\` is less than 3 characters, do not translate it and keep it as is.
- Field \`hint\` should be translated in-place.
- Use alphabet and writing system of the target language.
- Use terminology that is commonly used in the target language for software interfaces, especially in the context of Stable Diffusion and Generative AI.
- Do not leave non-translated words in the output, except for technical terms that do not have a widely accepted translation in the target language. In such cases, provide a transliteration or a brief explanation in parentheses.
- Ensure that all lines are present in the output, and that the output is valid JSON matching the provided schema.
`;
const languages = {
tb: 'English: Techno-Babble',
nb: 'English: For-N00bs',
hr: 'Croatian',
es: 'Spanish',
it: 'Italian',
xx: 'Esperanto',
qq: 'Latin',
fr: 'French',
de: 'German',
pt: 'Portuguese',
ru: 'Russian',
zh: 'Chinese',
ja: 'Japanese',
ko: 'Korean',
hi: 'Hindi',
ar: 'Arabic',
bn: 'Bengali',
ur: 'Urdu',
id: 'Indonesian',
vi: 'Vietnamese',
tr: 'Turkish',
sr: 'Serbian',
po: 'Polish',
he: 'Hebrew',
tlh: 'Klingon',
};
const responseSchema = {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
id: { type: Type.INTEGER, description: 'id of the item' },
label: { type: Type.STRING, description: 'original label of the item' },
localized: { type: Type.STRING, description: 'translated label of the item' },
reload: { type: Type.STRING, description: 'n/a' },
hint: { type: Type.STRING, description: 'long hint for the item' },
},
required: ['id', 'label', 'localized', 'reload', 'hint'],
},
};
async function localize() {
if (!apiKey || apiKey.length < 10) {
console.error('localize: set GOOGLE_API_KEY env variable with your API key');
process.exit();
}
const httpOptions = { timeout: 60000 };
const ai = new GoogleGenAI({ apiKey, httpOptions });
const thinkingConfig = {
includeThoughts: false,
thinkingLevel: ThinkingLevel.LOW,
};
const params = {
model,
contents: {
parts: [
{ text: 'prompt' },
{ text: 'data' },
],
},
config: {
responseMimeType: 'application/json',
thinkingConfig,
responseSchema,
},
};
console.log('params:', params);
const raw = fs.readFileSync('html/locale_en.json');
console.log('raw:', { bytes: raw.length });
const json = JSON.parse(raw);
console.log('targets:', { lang: Object.keys(languages), count: Object.keys(languages).length });
for (const index in Object.keys(languages)) {
const locale = Object.keys(languages)[index];
const lang = languages[locale];
const langPrompt = prompt.replace('{language}', lang).trim();
const output = {};
const fn = `html/locale_${locale}.json`;
if (fs.existsSync(fn)) {
console.log('skip:', { index, locale, lang, fn });
continue;
}
console.log('localize:', { index, locale, lang, fn });
const t0 = performance.now();
let allOk = true;
for (const section of Object.keys(json)) {
const keys = Object.keys(json[section]).length;
console.log(' start:', { locale, section, keys });
try {
const t1 = performance.now();
const sectionJSON = json[section];
const sectionParams = { ...params };
sectionParams.contents.parts[0].text = langPrompt;
sectionParams.contents.parts[1].text = `## JSON Data: \n${JSON.stringify(sectionJSON)}`;
const response = await ai.models.generateContent(sectionParams);
const responseJSON = JSON.parse(response.text);
const diff = Math.abs(keys - responseJSON.length);
if (diff > 1) {
console.error(' error:', { locale, section, input: keys, output: responseJSON.length });
allOk = false;
continue;
}
output[section] = JSON.parse(response.text);
const t2 = performance.now();
const kps = Math.round(1000000 * keys / (t2 - t1)) / 1000;
console.log(' end:', { locale, section, time: Math.round(t2 - t1) / 1000, kps });
} catch (err) {
allOk = false;
console.error(' error:', err);
process.exit(1);
}
// break; // for testing, remove this to process all sections
}
if (allOk) {
const txt = JSON.stringify(output, null, 2);
fs.writeFileSync(fn, txt);
}
const t3 = performance.now();
console.log(' time:', { locale, time: Math.round(t3 - t0) / 1000 });
}
}
localize();

2
wiki

@ -1 +1 @@
Subproject commit 64e9a355dc17eef9e237f7d3c9c4bde09e5aa6b2
Subproject commit 8afa5d9c71b06d3cd7b81e8f6099d0d88e021c05