refactor(i18n): reply to upstream's #2935 feedback

This commit is contained in:
anomixer
2026-07-08 01:51:36 +08:00
parent dd3cadbd81
commit a6c975f92c
9 changed files with 124 additions and 61 deletions

View File

@@ -12,7 +12,7 @@
</p> </p>
<p align="center"> <p align="center">
[English] | [<a href="docs/zh/README.md">中文</a>] | [<a href="docs/zh/README-tw.md">繁體中文</a>] | [<a href="docs/ja/README.md">日本語</a>] [English] | [<a href="docs/zh/README.md">中文</a>] | [<a href="docs/zh/README-Hant.md">繁體中文</a>] | [<a href="docs/ja/README.md">日本語</a>]
</p> </p>
**CowAgent** is an open-source super AI assistant that proactively plans tasks, controls your computer and external services, creates and runs Skills, builds a personal knowledge base and long-term memory, and grows alongside you through self-evolution — a reference implementation of Agent Harness engineering. **CowAgent** is an open-source super AI assistant that proactively plans tasks, controls your computer and external services, creates and runs Skills, builds a personal knowledge base and long-term memory, and grows alongside you through self-evolution — a reference implementation of Agent Harness engineering.

View File

@@ -47,13 +47,15 @@
This runs synchronously in <head> so the correct class is on <html> This runs synchronously in <head> so the correct class is on <html>
before any CSS or body rendering occurs. --> before any CSS or body rendering occurs. -->
<script> <script>
// Map an arbitrary locale string (zh-CN, en-US, fr ...) to 'zh' / 'zh-tw' / 'en', // Map an arbitrary locale string (zh-CN, en-US, fr ...) to 'zh' / 'zh-Hant' / 'en',
// or '' when unrecognized so callers can fall through to the next source. // or '' when unrecognized so callers can fall through to the next source.
window.__cowNormalizeLang__ = function(raw) { window.__cowNormalizeLang__ = function(raw) {
if (!raw) return ''; if (!raw) return '';
var v = String(raw).trim().toLowerCase().replace('_', '-'); var v = String(raw).trim().toLowerCase().replace('_', '-');
if (v === 'auto') return ''; if (v === 'auto') return '';
if (v.indexOf('zh-tw') === 0 || v.indexOf('zh-hk') === 0 || v.indexOf('zh-hant') >= 0) return 'zh-tw'; // Handle Traditional Chinese variants first (more specific)
if (v === 'zh-hant' || v.indexOf('zh-hant-') === 0 || v === 'zh-tw' || v === 'zh-hk') return 'zh-Hant';
// Then Simplified Chinese
if (v.indexOf('zh') === 0) return 'zh'; if (v.indexOf('zh') === 0) return 'zh';
if (v.indexOf('en') === 0) return 'en'; if (v.indexOf('en') === 0) return 'en';
return ''; return '';

View File

@@ -147,8 +147,9 @@ const I18N = {
config_custom_tip: '接口需遵循 OpenAI API 协议', config_custom_tip: '接口需遵循 OpenAI API 协议',
config_security: '安全设置', config_password: '访问密码', config_security: '安全设置', config_password: '访问密码',
config_password_hint: '留空则不启用密码保护', config_password_hint: '留空则不启用密码保护',
config_password_changed: '密码已更新,请重新登录', config_password_changed: '密码已更新',
config_password_cleared: '密码已清除', config_password_cleared: '密码已清除',
config_password_security_warning: '⚠️ 警告:目前密码为空且对外连接埠开放,建议重启服务,或检查是否调整监听位址绑定。',
skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能广场', skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能广场',
skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处', skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处',
tools_section_title: '内置工具', tools_loading: '加载工具中...', tools_section_title: '内置工具', tools_loading: '加载工具中...',
@@ -256,7 +257,7 @@ const I18N = {
edit_cancel: '取消', edit_cancel: '取消',
logout: '退出', logout: '退出',
}, },
'zh-tw': { 'zh-Hant': {
console: '控制台', console: '控制台',
nav_chat: '對話', nav_manage: '管理', nav_monitor: '監控', nav_chat: '對話', nav_manage: '管理', nav_monitor: '監控',
@@ -393,8 +394,9 @@ const I18N = {
config_custom_tip: '介面需遵循 OpenAI API 協議', config_custom_tip: '介面需遵循 OpenAI API 協議',
config_security: '安全設定', config_password: '訪問密碼', config_security: '安全設定', config_password: '訪問密碼',
config_password_hint: '留空則不啟用密碼保護', config_password_hint: '留空則不啟用密碼保護',
config_password_changed: '密碼已更新,請重新登入', config_password_changed: '密碼已更新',
config_password_cleared: '密碼已清除', config_password_cleared: '密碼已清除',
config_password_security_warning: '⚠️ 警告:目前密碼為空且對外連接埠開放,建議重啟服務,或檢查是否調整監聽位址綁定。',
skills_title: '技能管理', skills_desc: '檢視、啟用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能廣場', skills_title: '技能管理', skills_desc: '檢視、啟用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能廣場',
skills_loading: '載入技能中...', skills_loading_desc: '技能載入後將顯示在此處', skills_loading: '載入技能中...', skills_loading_desc: '技能載入後將顯示在此處',
tools_section_title: '內建工具', tools_loading: '載入工具中...', tools_section_title: '內建工具', tools_loading: '載入工具中...',
@@ -638,8 +640,9 @@ const I18N = {
config_custom_tip: 'API must follow OpenAI protocol.', config_custom_tip: 'API must follow OpenAI protocol.',
config_security: 'Security', config_password: 'Password', config_security: 'Security', config_password: 'Password',
config_password_hint: 'Leave empty to disable password protection', config_password_hint: 'Leave empty to disable password protection',
config_password_changed: 'Password updated, please re-login', config_password_changed: 'Password updated',
config_password_cleared: 'Password cleared', config_password_cleared: 'Password cleared',
config_password_security_warning: '⚠️ Warning: Password is now empty and the port is exposed. Consider restarting the service or adjusting the listening address binding.',
skills_title: 'Skills', skills_desc: 'View, enable, or disable agent tools and skills', skills_hub_btn: 'Skill Hub', skills_title: 'Skills', skills_desc: 'View, enable, or disable agent tools and skills', skills_hub_btn: 'Skill Hub',
skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading', skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading',
tools_section_title: 'Built-in Tools', tools_loading: 'Loading tools...', tools_section_title: 'Built-in Tools', tools_loading: 'Loading tools...',
@@ -759,6 +762,9 @@ let currentLang = (typeof window.__cowResolveLang__ === 'function')
if (!raw) return ''; if (!raw) return '';
const v = String(raw).trim().toLowerCase(); const v = String(raw).trim().toLowerCase();
if (v === 'auto') return ''; if (v === 'auto') return '';
// Handle Traditional Chinese variants first (more specific)
if (v === 'zh-hant' || v.startsWith('zh-hant-') || v === 'zh-tw' || v === 'zh-hk') return 'zh-Hant';
// Then Simplified Chinese
if (v.indexOf('zh') === 0) return 'zh'; if (v.indexOf('zh') === 0) return 'zh';
if (v.indexOf('en') === 0) return 'en'; if (v.indexOf('en') === 0) return 'en';
return ''; return '';
@@ -799,9 +805,15 @@ function applyI18n() {
el.setAttribute('data-tooltip', t(el.dataset.tipKey)); el.setAttribute('data-tooltip', t(el.dataset.tipKey));
}); });
installCfgTipPortal(); installCfgTipPortal();
// Clear any status messages when language changes
document.querySelectorAll('[id$="-status"]').forEach(el => {
el.classList.add('opacity-0');
});
const langLabel = document.getElementById('lang-label'); const langLabel = document.getElementById('lang-label');
if (langLabel) { if (langLabel) {
if (currentLang === 'zh-tw') langLabel.textContent = '繁中'; if (currentLang === 'zh-Hant') langLabel.textContent = '繁中';
else if (currentLang === 'zh') langLabel.textContent = '簡中'; else if (currentLang === 'zh') langLabel.textContent = '簡中';
else langLabel.textContent = 'EN'; else langLabel.textContent = 'EN';
} }
@@ -814,7 +826,7 @@ function applyI18n() {
// persists the user choice locally, re-renders the UI, and binds the choice to // 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. // the backend `cow_lang` config so logs / agent replies / CLI follow suit.
function setLanguage(lang) { function setLanguage(lang) {
const next = (lang === 'en' || lang === 'zh' || lang === 'zh-tw') ? lang : 'zh'; const next = (lang === 'en' || lang === 'zh' || lang === 'zh-Hant') ? lang : 'zh';
if (next === currentLang) { if (next === currentLang) {
// Still persist + sync in case storage/backend drifted from the UI. // Still persist + sync in case storage/backend drifted from the UI.
syncLanguageToBackend(next); syncLanguageToBackend(next);
@@ -855,7 +867,7 @@ function syncLanguageToBackend(lang, callback) {
function updateLangControls() { function updateLangControls() {
const langLabel = document.getElementById('lang-label'); const langLabel = document.getElementById('lang-label');
if (langLabel) { if (langLabel) {
if (currentLang === 'zh-tw') langLabel.textContent = '繁體'; if (currentLang === 'zh-Hant') langLabel.textContent = '繁體';
else if (currentLang === 'zh') langLabel.textContent = '簡體'; else if (currentLang === 'zh') langLabel.textContent = '簡體';
else langLabel.textContent = 'EN'; else langLabel.textContent = 'EN';
} }
@@ -866,7 +878,7 @@ function updateLangControls() {
sel._ddValue = currentLang; sel._ddValue = currentLang;
const textEl = sel.querySelector('.cfg-dropdown-text'); const textEl = sel.querySelector('.cfg-dropdown-text');
if (textEl) { if (textEl) {
if (currentLang === 'zh-tw') textEl.textContent = '繁體中文'; if (currentLang === 'zh-Hant') textEl.textContent = '繁體中文';
else if (currentLang === 'zh') textEl.textContent = '简体中文'; else if (currentLang === 'zh') textEl.textContent = '简体中文';
else textEl.textContent = 'English'; else textEl.textContent = 'English';
} }
@@ -878,8 +890,8 @@ function updateLangControls() {
function toggleLanguage() { function toggleLanguage() {
if (currentLang === 'zh') { if (currentLang === 'zh') {
setLanguage('zh-tw'); setLanguage('zh-Hant');
} else if (currentLang === 'zh-tw') { } else if (currentLang === 'zh-Hant') {
setLanguage('en'); setLanguage('en');
} else { } else {
setLanguage('zh'); setLanguage('zh');
@@ -1018,6 +1030,12 @@ function navigateTo(viewId) {
document.getElementById('breadcrumb-page').textContent = t(meta.page); document.getElementById('breadcrumb-page').textContent = t(meta.page);
document.getElementById('breadcrumb-page').dataset.i18n = meta.page; document.getElementById('breadcrumb-page').dataset.i18n = meta.page;
currentView = viewId; currentView = viewId;
// Clear status messages when navigating away
document.querySelectorAll('[id$="-status"]').forEach(el => {
el.classList.add('opacity-0');
});
if (window.innerWidth < 1024) closeSidebar(); if (window.innerWidth < 1024) closeSidebar();
} }
@@ -4554,7 +4572,7 @@ function initConfigView(data) {
if (langSel) { if (langSel) {
initDropdown( initDropdown(
langSel, langSel,
[{ value: 'zh', label: '简体中文' }, { value: 'zh-tw', label: '繁體中文' }, { value: 'en', label: 'English' }], [{ value: 'zh', label: '简体中文' }, { value: 'zh-Hant', label: '繁體中文' }, { value: 'en', label: 'English' }],
currentLang, currentLang,
(val) => setLanguage(val) (val) => setLanguage(val)
); );
@@ -4727,7 +4745,10 @@ function showStatus(elId, msgKey, isError) {
el.classList.toggle('text-red-500', !!isError); el.classList.toggle('text-red-500', !!isError);
el.classList.toggle('text-primary-500', !isError); el.classList.toggle('text-primary-500', !isError);
el.classList.remove('opacity-0'); el.classList.remove('opacity-0');
// Warning messages (errors) should stay visible, success messages auto-hide
if (!isError) {
setTimeout(() => el.classList.add('opacity-0'), 2500); setTimeout(() => el.classList.add('opacity-0'), 2500);
}
} }
function saveModelConfig() { function saveModelConfig() {
@@ -4840,18 +4861,33 @@ function savePasswordConfig() {
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
console.log('[Password Config] Response:', data); // Debug
if (data.status === 'success') { if (data.status === 'success') {
if (newPwd) { if (newPwd) {
showStatus('cfg-password-status', 'config_password_changed', false); showStatus('cfg-password-status', 'config_password_changed', false);
setTimeout(() => { window.location.reload(); }, 1500); // Mark as masked so user needs to re-enter to change again
input.dataset.masked = '1';
input.dataset.maskedVal = newPwd;
input.value = '••••••••';
input.classList.add('cfg-key-masked');
// Show logout button since password is now enabled
const logoutBtn = document.getElementById('logout-btn-header');
if (logoutBtn) logoutBtn.classList.remove('hidden');
} else { } else {
input.dataset.masked = ''; input.dataset.masked = '';
input.dataset.maskedVal = ''; input.dataset.maskedVal = '';
input.classList.remove('cfg-key-masked'); input.classList.remove('cfg-key-masked');
// Show security warning if password was cleared with public host
if (data.warning === 'password_cleared_with_public_host') {
showStatus('cfg-password-status', 'config_password_security_warning', true);
} else {
showStatus('cfg-password-status', 'config_password_cleared', false); showStatus('cfg-password-status', 'config_password_cleared', false);
}
const logoutBtn = document.getElementById('logout-btn-header'); const logoutBtn = document.getElementById('logout-btn-header');
if (logoutBtn) logoutBtn.classList.add('hidden'); if (logoutBtn) logoutBtn.classList.add('hidden');
setTimeout(() => { window.location.reload(); }, 1500);
} }
} else { } else {
showStatus('cfg-password-status', 'config_save_error', true); showStatus('cfg-password-status', 'config_save_error', true);
@@ -9019,7 +9055,7 @@ function showLoginScreen() {
if (currentLang === 'en') { if (currentLang === 'en') {
subtitle.textContent = 'Enter password to access the console'; subtitle.textContent = 'Enter password to access the console';
loginBtn.textContent = 'Login'; loginBtn.textContent = 'Login';
} else if (currentLang === 'zh-tw') { } else if (currentLang === 'zh-Hant') {
subtitle.textContent = '請輸入密碼以存取控制台'; subtitle.textContent = '請輸入密碼以存取控制台';
loginBtn.textContent = '登入'; loginBtn.textContent = '登入';
} else { } else {
@@ -9052,7 +9088,7 @@ function showLoginScreen() {
if (logoutBtn) logoutBtn.classList.remove('hidden'); if (logoutBtn) logoutBtn.classList.remove('hidden');
initApp(); initApp();
} else { } else {
if (currentLang === 'zh-tw') { if (currentLang === 'zh-Hant') {
errEl.textContent = '密碼錯誤'; errEl.textContent = '密碼錯誤';
} else if (currentLang === 'zh') { } else if (currentLang === 'zh') {
errEl.textContent = '密码错误'; errEl.textContent = '密码错误';
@@ -9065,7 +9101,7 @@ function showLoginScreen() {
} }
btn.disabled = false; btn.disabled = false;
}).catch(() => { }).catch(() => {
if (currentLang === 'zh-tw') { if (currentLang === 'zh-Hant') {
errEl.textContent = '網路錯誤,請重試'; errEl.textContent = '網路錯誤,請重試';
} else if (currentLang === 'zh') { } else if (currentLang === 'zh') {
errEl.textContent = '网络错误,请重试'; errEl.textContent = '网络错误,请重试';

View File

@@ -1854,9 +1854,13 @@ class ConfigHandler:
return json.dumps({"status": "error", "message": "no valid keys to update"}) return json.dumps({"status": "error", "message": "no valid keys to update"})
config_path = os.path.join(get_data_root(), "config.json") config_path = os.path.join(get_data_root(), "config.json")
old_password = "" # Store old password before update
if os.path.exists(config_path): if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f: with open(config_path, "r", encoding="utf-8") as f:
file_cfg = json.load(f) file_cfg = json.load(f)
# Capture old password before updating
if "web_password" in applied:
old_password = file_cfg.get("web_password", "")
else: else:
file_cfg = {} file_cfg = {}
file_cfg.update(applied) file_cfg.update(applied)
@@ -1864,7 +1868,6 @@ class ConfigHandler:
json.dump(file_cfg, f, indent=4, ensure_ascii=False) json.dump(file_cfg, f, indent=4, ensure_ascii=False)
logger.info(f"[WebChannel] Config updated: {list(applied.keys())}") logger.info(f"[WebChannel] Config updated: {list(applied.keys())}")
restarted = False
# Apply a language change immediately so backend logs, agent # Apply a language change immediately so backend logs, agent
# replies and CLI output switch without a restart. # replies and CLI output switch without a restart.
@@ -1875,25 +1878,25 @@ class ConfigHandler:
except Exception as lang_err: except Exception as lang_err:
logger.warning(f"[WebChannel] Failed to apply language: {lang_err}") logger.warning(f"[WebChannel] Failed to apply language: {lang_err}")
# Trigger a dynamic restart of the web channel if web_password is changed, # Check if password was cleared: if there was a password before clearing,
# so the socket re-binds to either 127.0.0.1 or 0.0.0.0 immediately. # the service is likely bound to 0.0.0.0 (public), so warn the user.
password_warning = None
if "web_password" in applied: if "web_password" in applied:
try: new_password = applied["web_password"]
import sys configured_host = file_cfg.get("web_host", "")
app_module = sys.modules.get('__main__') or sys.modules.get('app')
mgr = getattr(app_module, '_channel_mgr', None) if app_module else None # If password was cleared and there was a password before
if mgr: if not new_password and old_password:
def delay_restart(): # If web_host is not explicitly set, the service auto-binds based on password
time.sleep(0.5) # With password → 0.0.0.0 (public), without password → 127.0.0.1 (local)
mgr.restart("web") # So clearing password when it was previously set means going from public to local
threading.Thread( if not configured_host or configured_host == "0.0.0.0":
target=delay_restart, password_warning = "password_cleared_with_public_host"
daemon=True, logger.warning(
).start() "[WebChannel] Password cleared while service is likely bound to 0.0.0.0. "
restarted = True "Consider restarting the service to rebind to 127.0.0.1 "
logger.info("[WebChannel] Web channel restart scheduled due to password change") "or explicitly set web_host in config to prevent unauthorized access."
except Exception as restart_err: )
logger.warning(f"[WebChannel] Failed to trigger web channel restart: {restart_err}")
# Reset Bridge so that bot routing reflects the new config. # Reset Bridge so that bot routing reflects the new config.
# Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot) # Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot)
@@ -1907,7 +1910,7 @@ class ConfigHandler:
except Exception as reset_err: except Exception as reset_err:
logger.warning(f"[WebChannel] Failed to reset bridge: {reset_err}") logger.warning(f"[WebChannel] Failed to reset bridge: {reset_err}")
return json.dumps({"status": "success", "applied": applied, "restarted": restarted}, ensure_ascii=False) return json.dumps({"status": "success", "applied": applied, "warning": password_warning}, ensure_ascii=False)
except Exception as e: except Exception as e:
logger.error(f"Error updating config: {e}") logger.error(f"Error updating config: {e}")
return json.dumps({"status": "error", "message": str(e)}) return json.dumps({"status": "error", "message": str(e)})
@@ -3694,7 +3697,7 @@ class ChannelsHandler:
# Desktop build ships without lark-oapi, so hide Feishu from the list. # Desktop build ships without lark-oapi, so hide Feishu from the list.
desktop_mode = os.environ.get("COW_DESKTOP") == "1" desktop_mode = os.environ.get("COW_DESKTOP") == "1"
channels = [] channels = []
is_tw = i18n.get_language() == i18n.ZH_TW is_hant = i18n.get_language() == i18n.ZH_HANT
for ch_name, ch_def in self.CHANNEL_DEFS.items(): for ch_name, ch_def in self.CHANNEL_DEFS.items():
if desktop_mode and ch_name == "feishu": if desktop_mode and ch_name == "feishu":
continue continue
@@ -3707,11 +3710,11 @@ class ChannelsHandler:
display_val = raw_val display_val = raw_val
label_val = f["label"] label_val = f["label"]
if is_tw and isinstance(label_val, str): if is_hant and isinstance(label_val, str):
label_val = i18n.to_traditional(label_val) label_val = i18n.to_traditional(label_val)
elif is_tw and isinstance(label_val, dict): elif is_hant and isinstance(label_val, dict):
label_val = label_val.copy() label_val = label_val.copy()
label_val["zh-tw"] = i18n.to_traditional(label_val.get("zh", "")) label_val["zh-Hant"] = i18n.to_traditional(label_val.get("zh", ""))
fields_out.append({ fields_out.append({
"key": f["key"], "key": f["key"],
@@ -3722,11 +3725,11 @@ class ChannelsHandler:
}) })
label_val = ch_def["label"] label_val = ch_def["label"]
if is_tw and isinstance(label_val, str): if is_hant and isinstance(label_val, str):
label_val = i18n.to_traditional(label_val) label_val = i18n.to_traditional(label_val)
elif is_tw and isinstance(label_val, dict): elif is_hant and isinstance(label_val, dict):
label_val = label_val.copy() label_val = label_val.copy()
label_val["zh-tw"] = i18n.to_traditional(label_val.get("zh", "")) label_val["zh-Hant"] = i18n.to_traditional(label_val.get("zh", ""))
ch_info = { ch_info = {
"name": ch_name, "name": ch_name,
@@ -4297,7 +4300,7 @@ class ToolsHandler:
try: try:
instance = cls() instance = cls()
desc = instance.description desc = instance.description
if lang == i18n.ZH_TW and desc: if lang == i18n.ZH_HANT and desc:
desc = i18n.to_traditional(desc) desc = i18n.to_traditional(desc)
elif lang == "en" and name == "scheduler": elif lang == "en" and name == "scheduler":
desc = ( desc = (
@@ -4328,7 +4331,7 @@ class SkillsHandler:
manager = SkillManager(custom_dir=os.path.join(workspace_root, "skills")) manager = SkillManager(custom_dir=os.path.join(workspace_root, "skills"))
service = SkillService(manager) service = SkillService(manager)
skills = service.query() skills = service.query()
if i18n.get_language() == i18n.ZH_TW: if i18n.get_language() == i18n.ZH_HANT:
for skill in skills: for skill in skills:
if isinstance(skill, dict): if isinstance(skill, dict):
for k, v in list(skill.items()): for k, v in list(skill.items()):

View File

@@ -7,6 +7,11 @@ across the CLI, startup logs, error messages, agent prompts and channel
replies. It must NOT import project config (to avoid circular imports) and replies. It must NOT import project config (to avoid circular imports) and
must stay dependency-free so it can run at the earliest startup phase. must stay dependency-free so it can run at the earliest startup phase.
Supported language codes (BCP 47 compliant):
- "zh" (Simplified Chinese)
- "zh-Hant" (Traditional Chinese, script-based tag per Unicode CLDR)
- "en" (English)
Resolution priority (highest first): Resolution priority (highest first):
1. Explicit `cow_lang` from config.json — also covers Docker/CI, since any 1. Explicit `cow_lang` from config.json — also covers Docker/CI, since any
config key is overridable via its uppercase env var (e.g. COW_LANG=zh), config key is overridable via its uppercase env var (e.g. COW_LANG=zh),
@@ -19,7 +24,10 @@ Resolution priority (highest first):
5. Default -> English 5. Default -> English
A value of "auto" (the default) triggers detection (steps 2-5). Explicitly A value of "auto" (the default) triggers detection (steps 2-5). Explicitly
setting "zh" or "en" locks the language and skips detection. setting "zh", "zh-Hant", or "en" locks the language and skips detection.
Note: For backwards compatibility, zh-tw, zh-hk, and other regional variants
are automatically normalized to zh-Hant during detection.
""" """
import os import os
@@ -28,9 +36,9 @@ import sys
# Supported language codes # Supported language codes
ZH = "zh" ZH = "zh"
ZH_TW = "zh-tw" ZH_HANT = "zh-Hant"
EN = "en" EN = "en"
SUPPORTED = (ZH, ZH_TW, EN) SUPPORTED = (ZH, ZH_HANT, EN)
DEFAULT_LANG = EN DEFAULT_LANG = EN
# Mapped Simplified to Traditional characters in this codebase # Mapped Simplified to Traditional characters in this codebase
@@ -71,6 +79,20 @@ _PHRASE_MAP = {
def to_traditional(text): def to_traditional(text):
"""Convert Simplified Chinese text to Traditional Chinese.
Uses a two-tier approach:
1. Phrase-level mapping for project-specific terms (e.g., "配置""設定")
2. OpenCC library (opencc-python-reimplemented) if available for high-quality
context-aware conversion, with fallback to built-in character mapping
This function is designed to work without external dependencies. If OpenCC
is not installed, it falls back to a curated 450-character mapping table
plus 30+ technical term mappings that cover common project vocabulary.
For production use with zh-Hant language, installing OpenCC is recommended:
pip install opencc-python-reimplemented
"""
if not text: if not text:
return text return text
@@ -111,7 +133,7 @@ def _normalize(raw):
return None return None
# Traditional Chinese variants: zh-tw, zh-hk, zh-hant, zh-hant-tw, zh-hant-hk... # Traditional Chinese variants: zh-tw, zh-hk, zh-hant, zh-hant-tw, zh-hant-hk...
if value.startswith("zh-tw") or value.startswith("zh-hk") or "hant" in value: if value.startswith("zh-tw") or value.startswith("zh-hk") or "hant" in value:
return ZH_TW return ZH_HANT
# General or Simplified Chinese variants: zh, zh-cn, zh-hans... # General or Simplified Chinese variants: zh, zh-cn, zh-hans...
if value.startswith("zh") or value.startswith("chinese"): if value.startswith("zh") or value.startswith("chinese"):
return ZH return ZH
@@ -231,7 +253,7 @@ def get_language():
def is_zh(): def is_zh():
return get_language() in (ZH, ZH_TW) return get_language() in (ZH, ZH_HANT)
def t(zh_text, en_text): def t(zh_text, en_text):
@@ -241,6 +263,6 @@ def t(zh_text, en_text):
t("已中止", "Cancelled") t("已中止", "Cancelled")
""" """
lang = get_language() lang = get_language()
if lang == ZH_TW: if lang == ZH_HANT:
return to_traditional(zh_text) return to_traditional(zh_text)
return zh_text if lang == ZH else en_text return zh_text if lang == ZH else en_text

View File

@@ -12,7 +12,7 @@
</p> </p>
<p align="center"> <p align="center">
[<a href="../../README.md">English</a>] | [中文] | [<a href="README-tw.md">繁體中文</a>] | [<a href="../ja/README.md">日本語</a>] [<a href="../../README.md">English</a>] | [中文] | [<a href="README-Hant.md">繁體中文</a>] | [<a href="../ja/README.md">日本語</a>]
</p> </p>
**CowAgent** 是一个开源的超级 AI 助理,能够主动思考和规划任务、操作计算机和外部资源、创造和执行 Skills、构建知识库与长期记忆、通过自主进化与你一同成长是 Agent Harness 工程的最佳实践之一。 **CowAgent** 是一个开源的超级 AI 助理,能够主动思考和规划任务、操作计算机和外部资源、创造和执行 Skills、构建知识库与长期记忆、通过自主进化与你一同成长是 Agent Harness 工程的最佳实践之一。

View File

@@ -20,3 +20,7 @@ pypdf
python-docx python-docx
openpyxl openpyxl
python-pptx python-pptx
# i18n: Traditional Chinese (zh-Hant) high-quality conversion
# Built-in fallback provides basic conversion without this dependency
opencc-python-reimplemented

View File

@@ -36,7 +36,3 @@ discord.py
web.py; python_version < "3.13" web.py; python_version < "3.13"
web.py @ git+https://github.com/webpy/webpy.git ; python_version >= "3.13" web.py @ git+https://github.com/webpy/webpy.git ; python_version >= "3.13"
legacy-cgi; python_version >= "3.13" legacy-cgi; python_version >= "3.13"
# i18n translation mapping
opencc-python-reimplemented