From d42b6a6f10d0d5b1c701c9d0acb186de6b8620ea Mon Sep 17 00:00:00 2001 From: lpf Date: Tue, 3 Mar 2026 17:33:26 +0800 Subject: [PATCH] fix webui i18n --- webui/src/components/Header.tsx | 2 +- webui/src/i18n/index.ts | 226 +++++++++++++++++++++++++++++++- webui/src/pages/Config.tsx | 13 +- 3 files changed, 234 insertions(+), 7 deletions(-) diff --git a/webui/src/components/Header.tsx b/webui/src/components/Header.tsx index 2735660..0066378 100644 --- a/webui/src/components/Header.tsx +++ b/webui/src/components/Header.tsx @@ -47,7 +47,7 @@ const Header: React.FC = () => { className="flex items-center gap-2 text-sm font-medium text-zinc-400 hover:text-zinc-200 transition-colors bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 px-3 py-1.5 rounded-lg" > - {i18n.language === 'en' ? '中文' : 'English'} + {i18n.language === 'en' ? t('languageZh') : t('languageEn')} diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts index de4ee38..52bd753 100644 --- a/webui/src/i18n/index.ts +++ b/webui/src/i18n/index.ts @@ -223,6 +223,10 @@ const resources = { actionFailed: 'Action failed', cronExpressionPlaceholder: '*/5 * * * *', recipientId: 'recipient id', + languageZh: '中文', + languageEn: 'English', + configRoot: '(root)', + configCommaSeparatedHint: ', a, b', configLabels: { gateway: 'Gateway', host: 'Host', @@ -302,7 +306,114 @@ const resources = { factor: 'Factor', min_delay: 'Min Delay', max_delay: 'Max Delay', - jitter: 'Jitter' + jitter: 'Jitter', + channels: 'Channels', + cron: 'Cron', + workspace: 'Workspace', + proxy_fallbacks: 'Proxy Fallbacks', + heartbeat: 'Heartbeat', + every_sec: 'Interval (Seconds)', + ack_max_chars: 'Ack Max Chars', + prompt_template: 'Prompt Template', + autonomy: 'Autonomy', + tick_interval_sec: 'Tick Interval (Seconds)', + min_run_interval_sec: 'Min Run Interval (Seconds)', + max_pending_duration_sec: 'Max Pending Duration (Seconds)', + max_consecutive_stalls: 'Max Consecutive Stalls', + max_dispatch_per_tick: 'Max Dispatch Per Tick', + notify_cooldown_sec: 'Notify Cooldown (Seconds)', + notify_same_reason_cooldown_sec: 'Same-reason Notify Cooldown (Seconds)', + quiet_hours: 'Quiet Hours', + user_idle_resume_sec: 'User Idle Resume (Seconds)', + max_rounds_without_user: 'Max Rounds Without User', + task_history_retention_days: 'Task History Retention (Days)', + waiting_resume_debounce_sec: 'Waiting Resume Debounce (Seconds)', + idle_round_budget_release_sec: 'Idle Round Budget Release (Seconds)', + allowed_task_keywords: 'Allowed Task Keywords', + ekg_consecutive_error_threshold: 'EKG Consecutive Error Threshold', + texts: 'Text Templates', + no_response_fallback: 'No-response Fallback', + think_only_fallback: 'Think-only Fallback', + memory_recall_keywords: 'Memory Recall Keywords', + lang_usage: 'Language Usage Hint', + lang_invalid: 'Invalid Language Message', + lang_updated_template: 'Language Updated Template', + subagents_none: 'No-subagents Message', + sessions_none: 'No-sessions Message', + unsupported_action: 'Unsupported Action Message', + system_rewrite_template: 'System Rewrite Template', + runtime_compaction_note: 'Runtime Compaction Note', + startup_compaction_note: 'Startup Compaction Note', + autonomy_important_keywords: 'Autonomy Important Keywords', + autonomy_completion_template: 'Autonomy Completion Template', + autonomy_blocked_template: 'Autonomy Blocked Template', + context_compaction: 'Context Compaction', + mode: 'Mode', + trigger_messages: 'Trigger Messages', + keep_recent_messages: 'Keep Recent Messages', + max_summary_chars: 'Max Summary Chars', + max_transcript_chars: 'Max Transcript Chars', + runtime_control: 'Runtime Control', + intent_max_input_chars: 'Intent Max Input Chars', + autonomy_tick_interval_sec: 'Autonomy Tick Interval (Seconds)', + autonomy_min_run_interval_sec: 'Autonomy Min Run Interval (Seconds)', + autonomy_idle_threshold_sec: 'Autonomy Idle Threshold (Seconds)', + autonomy_max_rounds_without_user: 'Autonomy Max Rounds Without User', + autonomy_max_pending_duration_sec: 'Autonomy Max Pending Duration (Seconds)', + autonomy_max_consecutive_stalls: 'Autonomy Max Consecutive Stalls', + autolearn_max_rounds_without_user: 'Autolearn Max Rounds Without User', + run_state_ttl_seconds: 'Run State TTL (Seconds)', + run_state_max: 'Run State Max', + tool_parallel_safe_names: 'Tool Parallel Safe Names', + tool_max_parallel_calls: 'Tool Max Parallel Calls', + system_summary: 'System Summary', + marker: 'Summary Marker', + completed_prefix: 'Completed Prefix', + changes_prefix: 'Changes Prefix', + outcome_prefix: 'Outcome Prefix', + completed_title: 'Completed Title', + changes_title: 'Changes Title', + outcomes_title: 'Outcomes Title', + inbound_message_id_dedupe_ttl_seconds: 'Inbound Message Dedupe TTL (Seconds)', + inbound_content_dedupe_window_seconds: 'Inbound Content Dedupe Window (Seconds)', + outbound_dedupe_window_seconds: 'Outbound Dedupe Window (Seconds)', + telegram: 'Telegram', + allow_from: 'Allowed Senders', + allow_chats: 'Allowed Chats', + enable_groups: 'Enable Groups', + require_mention_in_groups: 'Require Mention In Groups', + discord: 'Discord', + maixcam: 'MaixCam', + whatsapp: 'WhatsApp', + bridge_url: 'Bridge URL', + feishu: 'Feishu', + app_id: 'App ID', + app_secret: 'App Secret', + encrypt_key: 'Encrypt Key', + verification_token: 'Verification Token', + dingtalk: 'DingTalk', + filesystem: 'Filesystem', + working_dir: 'Working Directory', + timeout: 'Timeout', + auto_install_missing: 'Auto-install Missing', + sandbox: 'Sandbox', + image: 'Image', + web: 'Web', + search: 'Search', + max_results: 'Max Results', + proxies: 'Proxies', + cross_session_call_id: 'Cross-session Call ID', + supports_responses_compact: 'Supports Responses Compact', + min_sleep_sec: 'Min Sleep (Seconds)', + max_sleep_sec: 'Max Sleep (Seconds)', + retry_backoff_base_sec: 'Retry Backoff Base (Seconds)', + retry_backoff_max_sec: 'Retry Backoff Max (Seconds)', + max_consecutive_failure_retries: 'Max Consecutive Failure Retries', + max_workers: 'Max Workers', + dir: 'Directory', + filename: 'Filename', + max_size_mb: 'Max Size (MB)', + retention_days: 'Retention Days' } } }, @@ -526,6 +637,10 @@ const resources = { actionFailed: '操作失败', cronExpressionPlaceholder: '*/5 * * * *', recipientId: '接收者 ID', + languageZh: '中文', + languageEn: 'English', + configRoot: '(根)', + configCommaSeparatedHint: ',例如 a,b', configLabels: { gateway: '网关', host: '主机', @@ -605,7 +720,114 @@ const resources = { factor: '因子', min_delay: '最小延迟', max_delay: '最大延迟', - jitter: '抖动' + jitter: '抖动', + channels: '通道', + cron: '定时任务', + workspace: '工作目录', + proxy_fallbacks: '代理回退链', + heartbeat: '心跳', + every_sec: '间隔(秒)', + ack_max_chars: '确认最大字符数', + prompt_template: '提示模板', + autonomy: '自治', + tick_interval_sec: '轮询间隔(秒)', + min_run_interval_sec: '最小运行间隔(秒)', + max_pending_duration_sec: '最大挂起时长(秒)', + max_consecutive_stalls: '最大连续停滞次数', + max_dispatch_per_tick: '每次轮询最大派发数', + notify_cooldown_sec: '通知冷却(秒)', + notify_same_reason_cooldown_sec: '同原因通知冷却(秒)', + quiet_hours: '静默时段', + user_idle_resume_sec: '用户空闲恢复(秒)', + max_rounds_without_user: '无用户最大轮数', + task_history_retention_days: '任务历史保留天数', + waiting_resume_debounce_sec: '等待恢复防抖(秒)', + idle_round_budget_release_sec: '空闲轮次预算释放(秒)', + allowed_task_keywords: '允许任务关键词', + ekg_consecutive_error_threshold: 'EKG 连续错误阈值', + texts: '文本模板', + no_response_fallback: '无响应兜底文案', + think_only_fallback: '仅思考兜底文案', + memory_recall_keywords: '记忆召回关键词', + lang_usage: '语言用法提示', + lang_invalid: '语言非法提示', + lang_updated_template: '语言更新模板', + subagents_none: '无子代理提示', + sessions_none: '无会话提示', + unsupported_action: '不支持操作提示', + system_rewrite_template: '系统改写模板', + runtime_compaction_note: '运行期压缩说明', + startup_compaction_note: '启动期压缩说明', + autonomy_important_keywords: '自治重要关键词', + autonomy_completion_template: '自治完成模板', + autonomy_blocked_template: '自治阻塞模板', + context_compaction: '上下文压缩', + mode: '模式', + trigger_messages: '触发消息数', + keep_recent_messages: '保留最近消息数', + max_summary_chars: '摘要最大字符数', + max_transcript_chars: '转录最大字符数', + runtime_control: '运行时控制', + intent_max_input_chars: '意图输入最大字符数', + autonomy_tick_interval_sec: '自治轮询间隔(秒)', + autonomy_min_run_interval_sec: '自治最小运行间隔(秒)', + autonomy_idle_threshold_sec: '自治空闲阈值(秒)', + autonomy_max_rounds_without_user: '自治无用户最大轮数', + autonomy_max_pending_duration_sec: '自治最大挂起时长(秒)', + autonomy_max_consecutive_stalls: '自治最大连续停滞次数', + autolearn_max_rounds_without_user: '自学习无用户最大轮数', + run_state_ttl_seconds: '运行状态 TTL(秒)', + run_state_max: '运行状态上限', + tool_parallel_safe_names: '工具并行安全名单', + tool_max_parallel_calls: '工具最大并行调用数', + system_summary: '系统摘要', + marker: '摘要标记', + completed_prefix: '完成前缀', + changes_prefix: '变更前缀', + outcome_prefix: '结果前缀', + completed_title: '完成标题', + changes_title: '变更标题', + outcomes_title: '结果标题', + inbound_message_id_dedupe_ttl_seconds: '入站消息去重 TTL(秒)', + inbound_content_dedupe_window_seconds: '入站内容去重窗口(秒)', + outbound_dedupe_window_seconds: '出站去重窗口(秒)', + telegram: 'Telegram', + allow_from: '允许发送者', + allow_chats: '允许会话', + enable_groups: '启用群组', + require_mention_in_groups: '群组需 @ 提及', + discord: 'Discord', + maixcam: 'MaixCam', + whatsapp: 'WhatsApp', + bridge_url: '桥接地址', + feishu: '飞书', + app_id: '应用 ID', + app_secret: '应用密钥', + encrypt_key: '加密 Key', + verification_token: '校验 Token', + dingtalk: '钉钉', + filesystem: '文件系统', + working_dir: '工作目录', + timeout: '超时', + auto_install_missing: '自动安装缺失依赖', + sandbox: '沙箱', + image: '镜像', + web: 'Web', + search: '搜索', + max_results: '最大结果数', + proxies: '代理集合', + cross_session_call_id: '跨会话调用 ID', + supports_responses_compact: '支持紧凑 responses', + min_sleep_sec: '最小休眠(秒)', + max_sleep_sec: '最大休眠(秒)', + retry_backoff_base_sec: '重试退避基准(秒)', + retry_backoff_max_sec: '重试退避上限(秒)', + max_consecutive_failure_retries: '最大连续失败重试次数', + max_workers: '最大 worker 数', + dir: '目录', + filename: '文件名', + max_size_mb: '最大大小(MB)', + retention_days: '保留天数' } } } diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx index 7266386..d923249 100644 --- a/webui/src/pages/Config.tsx +++ b/webui/src/pages/Config.tsx @@ -26,6 +26,11 @@ const Config: React.FC = () => { const [search, setSearch] = useState(''); const [newProxyName, setNewProxyName] = useState(''); + const configLabels = useMemo( + () => t('configLabels', { returnObjects: true }) as Record, + [t] + ); + const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]); const allTopKeys = useMemo(() => Object.keys(cfg || {}).filter(k => typeof (cfg as any)?.[k] === 'object' && (cfg as any)?.[k] !== null), [cfg]); @@ -63,7 +68,7 @@ const Config: React.FC = () => { const walk = (a: any, b: any, p: string) => { const keys = new Set([...(a && typeof a === 'object' ? Object.keys(a) : []), ...(b && typeof b === 'object' ? Object.keys(b) : [])]); if (keys.size === 0) { - if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ path: p || '(root)', before: a, after: b }); + if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ path: p || t('configRoot'), before: a, after: b }); return; } keys.forEach((k) => { @@ -193,7 +198,7 @@ const Config: React.FC = () => { onClick={() => setSelectedTop(k)} className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${activeTop === k ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-zinc-300 hover:bg-zinc-800/60'}`} > - {k} + {configLabels[k] || k} ))} @@ -216,7 +221,7 @@ const Config: React.FC = () => { updateProxyField(name, 'api_base', e.target.value)} placeholder={t('configLabels.api_base')} className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> updateProxyField(name, 'api_key', e.target.value)} placeholder={t('configLabels.api_key')} className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> updateProxyField(name, 'protocol', e.target.value)} placeholder={t('configLabels.protocol')} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> - updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')},a,b`} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> + updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')}${t('configCommaSeparatedHint')}`} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> ))} @@ -229,7 +234,7 @@ const Config: React.FC = () => { {activeTop ? ( } + labels={configLabels} path={activeTop} hotPaths={hotReloadFieldDetails.map((x) => x.path)} onlyHot={hotOnly}