From 1827a2a31c7cbefeab541cab9bb5a4f6a521213d Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sun, 31 May 2026 17:01:43 +0800 Subject: [PATCH] feat(i18n): bind web language switch to cow_lang config --- channel/web/chat.html | 25 +++++++++++++ channel/web/static/js/console.js | 64 +++++++++++++++++++++++++++++++- channel/web/web_channel.py | 10 +++++ cli/commands/process.py | 2 + plugins/cow_cli/cow_cli.py | 4 ++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/channel/web/chat.html b/channel/web/chat.html index 1bbf0f04..96bb67bd 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -659,6 +659,31 @@ + +
+
+
+ +
+

语言

+
+
+
+ +
+
+ -- + +
+
+
+
+
+
+ diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js index e5865051..a68da915 100644 --- a/channel/web/static/js/console.js +++ b/channel/web/static/js/console.js @@ -115,6 +115,7 @@ const I18N = { input_placeholder: '输入消息,或输入 / 使用指令', config_title: '配置管理', config_desc: '管理模型和 Agent 配置', config_model: '模型配置', config_agent: 'Agent 配置', + config_language: '语言', config_language_hint: '界面展示、命令文案、系统报错等使用的语言(与右上角切换同步)', config_model_advanced: '高级配置', config_channel: '通道配置', config_agent_enabled: 'Agent 模式', @@ -310,6 +311,7 @@ const I18N = { input_placeholder: 'Type a message, or press / for commands', config_title: 'Configuration', config_desc: 'Manage model and agent settings', config_model: 'Model Configuration', config_agent: 'Agent Configuration', + config_language: 'Language', config_language_hint: 'Language for the UI, command text, system messages and more (synced with the top-right switch)', config_model_advanced: 'Advanced', config_channel: 'Channel Configuration', config_agent_enabled: 'Agent Mode', @@ -454,14 +456,60 @@ function applyI18n() { if (langLabel) langLabel.textContent = currentLang === 'zh' ? '中文' : 'EN'; } -function toggleLanguage() { - currentLang = currentLang === 'zh' ? 'en' : 'zh'; +// Single entry point for switching language. Updates the in-memory language, +// persists the user choice locally, re-renders the UI, and binds the choice to +// the backend `cow_lang` config so logs / agent replies / CLI follow suit. +function setLanguage(lang) { + const next = (lang === 'en') ? 'en' : 'zh'; + if (next === currentLang) { + // Still persist + sync in case storage/backend drifted from the UI. + syncLanguageToBackend(next); + return; + } + currentLang = next; localStorage.setItem('cow_lang', currentLang); applyI18n(); _applyInputTooltips(); // Re-render views whose DOM is built in JS (data-i18n alone does not // cover strings interpolated via t() into innerHTML). try { rerenderDynamicViews(); } catch (e) {} + // Keep the language switch button and config selector visually in sync. + try { updateLangControls(); } catch (e) {} + syncLanguageToBackend(currentLang); +} + +// Persist the language to the backend `cow_lang` config (best-effort; the UI +// has already switched locally, so a network failure is non-blocking). +function syncLanguageToBackend(lang) { + try { + fetch('/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ updates: { cow_lang: lang } }) + }).catch(() => {}); + } catch (e) {} +} + +// Reflect the current language on both the top-right toggle and the config +// selector (if present), so the two entry points stay synchronized. +function updateLangControls() { + const langLabel = document.getElementById('lang-label'); + if (langLabel) langLabel.textContent = currentLang === 'zh' ? '中文' : 'EN'; + // The config language picker is the custom .cfg-dropdown component. Only + // sync it once it has been initialized (i.e. the config panel was opened). + const sel = document.getElementById('cfg-lang-select'); + if (sel && sel._ddValue !== undefined && sel._ddValue !== currentLang) { + sel._ddValue = currentLang; + const textEl = sel.querySelector('.cfg-dropdown-text'); + if (textEl) textEl.textContent = currentLang === 'zh' ? '中文' : 'English'; + sel.querySelectorAll('.cfg-dropdown-item').forEach(i => { + i.classList.toggle('active', i.dataset.value === currentLang); + }); + } +} + +function toggleLanguage() { + setLanguage(currentLang === 'zh' ? 'en' : 'zh'); } // Refresh JS-rendered views after a language switch. Each branch uses the @@ -3358,6 +3406,18 @@ function initConfigView(data) { document.getElementById('cfg-max-steps').value = data.agent_max_steps || 20; document.getElementById('cfg-enable-thinking').checked = data.enable_thinking === true; + // Reflect the current UI language (already resolved, may include the user's + // local choice) on the selector so it stays in sync with the top-right toggle. + const langSel = document.getElementById('cfg-lang-select'); + if (langSel) { + initDropdown( + langSel, + [{ value: 'zh', label: '中文' }, { value: 'en', label: 'English' }], + currentLang, + (val) => setLanguage(val) + ); + } + const pwdInput = document.getElementById('cfg-password'); const maskedPwd = data.web_password_masked || ''; pwdInput.value = maskedPwd; diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index e2c0c5e4..88471dfa 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -1535,6 +1535,7 @@ class ConfigHandler: ]) EDITABLE_KEYS = { + "cow_lang", "model", "bot_type", "use_linkai", "open_ai_api_base", "deepseek_api_base", "qianfan_api_base", "claude_api_base", "gemini_api_base", "zhipu_ai_api_base", "moonshot_base_url", "ark_base_url", "custom_api_base", "mimo_api_base", @@ -1643,6 +1644,15 @@ class ConfigHandler: logger.info(f"[WebChannel] Config updated: {list(applied.keys())}") + # Apply a language change immediately so backend logs, agent + # replies and CLI output switch without a restart. + if "cow_lang" in applied: + try: + i18n.resolve_language(applied["cow_lang"]) + logger.info(f"[WebChannel] Language switched to: {i18n.get_language()}") + except Exception as lang_err: + logger.warning(f"[WebChannel] Failed to apply language: {lang_err}") + # Reset Bridge so that bot routing reflects the new config. # Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot) # even after the user switches bot_type / use_linkai / model in UI. diff --git a/cli/commands/process.py b/cli/commands/process.py index 6ccffdcb..9d22b67f 100644 --- a/cli/commands/process.py +++ b/cli/commands/process.py @@ -298,6 +298,8 @@ def status(): click.echo(_t(f" 模型: {cfg.get('model', 'unknown')}", f" Model: {cfg.get('model', 'unknown')}")) mode = "Chat" if cfg.get("agent") is False else "Agent" click.echo(_t(f" 模式: {mode}", f" Mode: {mode}")) + lang_label = "中文" if i18n.get_language() == "zh" else "English" + click.echo(_t(f" 语言: {lang_label}", f" Language: {lang_label}")) @click.command() diff --git a/plugins/cow_cli/cow_cli.py b/plugins/cow_cli/cow_cli.py index fdd0081f..62bbc786 100644 --- a/plugins/cow_cli/cow_cli.py +++ b/plugins/cow_cli/cow_cli.py @@ -465,6 +465,10 @@ class CowCliPlugin(Plugin): mode = "Chat" if cfg.get("agent") is False else "Agent" lines.append(_t(f" 模式: {mode}", f" Mode: {mode}")) + from common import i18n + lang_label = "中文" if i18n.get_language() == "zh" else "English" + lines.append(_t(f" 语言: {lang_label}", f" Language: {lang_label}")) + session_id = self._get_session_id(e_context, fallback=session_id) agent = self._get_agent(session_id) if agent: