fix webui i18n

This commit is contained in:
lpf
2026-03-03 17:33:26 +08:00
parent 2357a5de9b
commit d42b6a6f10
3 changed files with 234 additions and 7 deletions

View File

@@ -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"
>
<Globe className="w-4 h-4" />
{i18n.language === 'en' ? '中文' : 'English'}
{i18n.language === 'en' ? t('languageZh') : t('languageEn')}
</button>
</div>
</header>

View File

@@ -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: '保留天数'
}
}
}

View File

@@ -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<string, string>,
[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}
</button>
))}
</div>
@@ -216,7 +221,7 @@ const Config: React.FC = () => {
<input value={String(p?.api_base || '')} onChange={(e)=>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" />
<input value={String(p?.api_key || '')} onChange={(e)=>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" />
<input value={String(p?.protocol || '')} onChange={(e)=>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" />
<input value={Array.isArray(p?.models) ? p.models.join(',') : ''} onChange={(e)=>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" />
<input value={Array.isArray(p?.models) ? p.models.join(',') : ''} onChange={(e)=>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" />
<button onClick={()=>removeProxy(name)} className="md:col-span-1 px-2 py-1 rounded bg-red-900/60 hover:bg-red-800 text-red-100">{t('delete')}</button>
</div>
))}
@@ -229,7 +234,7 @@ const Config: React.FC = () => {
{activeTop ? (
<RecursiveConfig
data={(cfg as any)?.[activeTop] || {}}
labels={t('configLabels', { returnObjects: true }) as Record<string, string>}
labels={configLabels}
path={activeTop}
hotPaths={hotReloadFieldDetails.map((x) => x.path)}
onlyHot={hotOnly}