Establish shared i18n foundation

This commit is contained in:
DemonGatanjieu
2026-08-06 16:20:53 +08:00
parent 68a979630c
commit 00aefb9eed
5 changed files with 82 additions and 16 deletions
+21
View File
@@ -1,5 +1,26 @@
# AI Changelog
## [Snapshot] 2026-08-06 - Establish shared i18n foundation and migrate dialog pilot
**Implemented**
- Added normalized locale handling and a shared translation contract to `web/modules/locales.js`: supported-locale discovery, locale normalization, safe fallback, parameter interpolation, and reusable translators.
- Added `dialogOk` and `dialogCancel` keys to the existing Chinese and English dictionaries.
- Updated `web/main.js` to consume the shared locale contract instead of reading the dictionary directly, while preserving the existing zh/en detection behavior.
- Migrated `web/modules/ui_dialog.js` to the shared translator for alert/confirm buttons. No larger UI module, Russian locale, or MIT license was changed in this snapshot.
**Validation**
- `node --check` passed for every JavaScript file under `web/`.
- Direct i18n foundation checks passed for locale normalization, unsupported-locale fallback, translation lookup, and missing-key fallback.
- `node tests/recipe_parser_roundtrip.mjs` passed.
- Python compilation passed for every `api/*.py` module and `scraper.py` using the bundled Python runtime.
- `git diff --check` passed.
**Next**
- Continue only with the next explicitly approved migration batch. The remaining inline branches stay unchanged so each later batch remains independently reviewable and reversible.
## [Plan Snapshot] 2026-08-06 - Full project audit and staged i18n plan
**Audited**
+1 -1
View File
@@ -50,7 +50,7 @@ Parameter Notebook actions include “Read current and create” and “Apply to
The extension is a UI-only ComfyUI integration: `NODE_CLASS_MAPPINGS` is intentionally empty, and the project does not provide or install custom nodes. Its runtime is split between Vanilla JS/CSS frontend modules and Python `aiohttp` routes hosted by ComfyUI. The repository currently has no bundled font, image, video, vendor library, dependency manifest, or license file; Civitai, optional DeepL/Google translation endpoints, ComfyUI host APIs, and Pillow/aiohttp are runtime/environment integrations rather than bundled project assets.
`web/modules/locales.js` is the canonical home for user-facing translation data. The current codebase still contains substantial inline `zh`/`en` branches across `main.js`, Sidebar, Detail, Gallery, Doctor, Notebook, Dialog, Grid, and the optional hash resolver. The i18n refactor must introduce one normalized translation contract with safe fallback and parameter interpolation before migrating those modules. It must preserve current Chinese and English behavior, keep model names/paths/node types/user data out of locale dictionaries, and keep translated HTML inside the existing `safe_dom.js` boundary. Missing translations must never prevent a panel or action from rendering.
`web/modules/locales.js` is the canonical home for user-facing translation data. The current codebase still contains substantial inline `zh`/`en` branches across `main.js`, Sidebar, Detail, Gallery, Doctor, Notebook, Dialog, Grid, and the optional hash resolver. The first i18n migration snapshot now provides `supportedLocales`, `normalizeLocale`, `resolveLocale`, `formatTranslation`, `translate`, and `createTranslator`. `main.js` and `ui_dialog.js` use this contract; the remaining modules are intentionally unmigrated until their own reviewable snapshots. The refactor must preserve current Chinese and English behavior, keep model names/paths/node types/user data out of locale dictionaries, and keep translated HTML inside the existing `safe_dom.js` boundary. Missing translations must never prevent a panel or action from rendering. Language selection remains the existing zh/en toggle until a later phase introduces a registered locale selector.
Every i18n or licensing change is a local, reviewable snapshot. The implementation must update this document and `.agents/logs/ai_changelog.md` in the same change, run proportional syntax/runtime checks, and create a local Git commit. Remote pushes require explicit user authorization. The MIT license applies to project-owned code and documentation only; external service content and host-project dependencies retain their own terms.
+6 -6
View File
@@ -6,7 +6,7 @@ import { showNotebooks, closeWorkspace, refreshNotebooks, saveCurrentNotebook, d
import { showRecipes, refreshRecipes, renderRecipeList, handleSaveRecipe } from './modules/ui_recipes.js';
import { initDoctorPanel, diagnoseNode, renderGlobalDashboard, initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, _openGalleryReplacer, openLoraInsertionPicker, runGlobalDoctorScan } from './modules/ui_doctor.js';
import { app } from "../../scripts/app.js";
import { i18n } from './modules/locales.js';
import { normalizeLocale, resolveLocale, translate } from './modules/locales.js';
// ============================================================================
// TABLE OF CONTENTS (TOC)
// 1. App Registration & Entry (Search for "app.registerExtension")
@@ -55,9 +55,9 @@ try {
defaultLang = 'en';
}
}
let currentLang = localStorage.getItem('anomalous_lang') || defaultLang;
window.anomalous_browser_lang = currentLang;
const t = (key) => i18n[currentLang][key] || key;
let currentLang = resolveLocale(localStorage.getItem('anomalous_lang') || defaultLang);
window.anomalous_browser_lang = currentLang;
const t = (key, params) => translate(key, params, window.anomalous_browser_lang || currentLang);
class AnomalousBrowser {
constructor() {
@@ -457,8 +457,8 @@ app.registerExtension({
if (app && app.ui && app.ui.settings) {
const locale = app.ui.settings.getSettingValue('Comfy.Locale') || app.ui.settings.getSettingValue('Comfy.Locale.Language');
if (locale) {
currentLang = locale.toLowerCase().includes('en') ? 'en' : 'zh';
window.anomalous_browser_lang = currentLang;
currentLang = normalizeLocale(locale) || defaultLang;
window.anomalous_browser_lang = currentLang;
}
}
} catch (e) { }
+47 -6
View File
@@ -68,8 +68,10 @@ export const i18n = {
sendToCanvas: '🚀 发送到画布',
translate: '翻译',
newNotebookName: '新笔记本名称',
noBaseModel: '请先选择 Base Model',
closeHelp: '关闭说明',
noBaseModel: '请先选择 Base Model',
dialogOk: '确定',
dialogCancel: '取消',
closeHelp: '关闭说明',
delSure: '⚠️ 确定吗?',
dockTitle: '侧边停靠',
replaceAll: '全部替换',
@@ -391,8 +393,10 @@ export const i18n = {
sendToCanvas: '🚀 Send to Canvas',
translate: 'Translate',
newNotebookName: 'New Notebook Name',
noBaseModel: 'Select a Base Model first',
closeHelp: 'Close Manual',
noBaseModel: 'Select a Base Model first',
dialogOk: 'OK',
dialogCancel: 'Cancel',
closeHelp: 'Close Manual',
delSure: '⚠️ Sure?',
dockTitle: 'Dock to Left',
replaceAll: 'Replace All',
@@ -644,5 +648,42 @@ export const i18n = {
recipeSaveModelPreviewSnapshots: 'Embed Model Thumbnails',
recipeSaveModelPreviewSnapshotsHint: 'Embed small static thumbnails of the models directly into the recipe file. When you share this recipe, others will be able to see the model covers even if they don\'t have the models locally. (Videos are excluded to save space)',
recipeDetailSavedSnapshot: 'Saved snapshot',
}
};
}
};
const DEFAULT_LOCALE = 'zh';
const FALLBACK_LOCALE = 'en';
const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
export const supportedLocales = Object.freeze(Object.keys(i18n));
export function normalizeLocale(value) {
const raw = String(value ?? '').trim().toLowerCase().replace(/_/g, '-');
if (!raw) return '';
const base = raw.split('-')[0];
return hasOwn(i18n, base) ? base : '';
}
export function resolveLocale(value = globalThis.anomalous_browser_lang) {
return normalizeLocale(value) || DEFAULT_LOCALE;
}
export function formatTranslation(template, params = {}) {
if (typeof template !== 'string') return template;
return template.replace(/\{([A-Za-z0-9_.-]+)\}/g, (token, key) => (
hasOwn(params, key) ? String(params[key]) : token
));
}
export function translate(key, params = {}, locale = resolveLocale()) {
const activeLocale = resolveLocale(locale);
const template = i18n[activeLocale]?.[key]
?? i18n[FALLBACK_LOCALE]?.[key]
?? i18n[DEFAULT_LOCALE]?.[key]
?? key;
return formatTranslation(template, params);
}
export function createTranslator(locale) {
return (key, params = {}) => translate(key, params, locale);
}
+7 -3
View File
@@ -1,3 +1,7 @@
import { translate } from './locales.js';
const t = (key, params) => translate(key, params);
export function anomalousAlert(message, title = 'Anomalous') {
return new Promise((resolve) => {
const overlay = document.createElement('div');
@@ -45,7 +49,7 @@ export function anomalousAlert(message, title = 'Anomalous') {
footer.style.marginTop = '8px';
const okBtn = document.createElement('button');
okBtn.textContent = window.anomalous_browser_lang === 'zh' ? '确定' : 'OK';
okBtn.textContent = t('dialogOk');
okBtn.className = 'anomalous-btn-primary';
okBtn.style.padding = '8px 24px';
@@ -112,12 +116,12 @@ export function anomalousConfirm(message, title = 'Anomalous', options = {}) {
footer.style.marginTop = '8px';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel';
cancelBtn.textContent = t('dialogCancel');
cancelBtn.className = 'anomalous-btn-ghost';
cancelBtn.style.padding = '8px 24px';
const okBtn = document.createElement('button');
okBtn.textContent = window.anomalous_browser_lang === 'zh' ? '确定' : 'OK';
okBtn.textContent = t('dialogOk');
okBtn.className = 'anomalous-btn-danger';
okBtn.style.padding = '8px 24px';