From 00aefb9eed142a1566eac6a6446212403fb47c41 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Thu, 6 Aug 2026 16:20:53 +0800 Subject: [PATCH] Establish shared i18n foundation --- .agents/logs/ai_changelog.md | 21 ++++++++++++++ ARCHITECTURE.md | 2 +- web/main.js | 12 ++++---- web/modules/locales.js | 53 ++++++++++++++++++++++++++++++++---- web/modules/ui_dialog.js | 10 +++++-- 5 files changed, 82 insertions(+), 16 deletions(-) diff --git a/.agents/logs/ai_changelog.md b/.agents/logs/ai_changelog.md index ed1902b..51615d1 100644 --- a/.agents/logs/ai_changelog.md +++ b/.agents/logs/ai_changelog.md @@ -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** diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2083a13..7ae1965 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. diff --git a/web/main.js b/web/main.js index ea10b50..75dfb76 100644 --- a/web/main.js +++ b/web/main.js @@ -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) { } diff --git a/web/modules/locales.js b/web/modules/locales.js index a0e5c02..a92e2c3 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -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); +} diff --git a/web/modules/ui_dialog.js b/web/modules/ui_dialog.js index 0c3a076..7e76751 100644 --- a/web/modules/ui_dialog.js +++ b/web/modules/ui_dialog.js @@ -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';