diff --git a/app/shared/src/chatview/components/ChatViewTranscript.tsx b/app/shared/src/chatview/components/ChatViewTranscript.tsx index b07b2de65..b4f16b363 100644 --- a/app/shared/src/chatview/components/ChatViewTranscript.tsx +++ b/app/shared/src/chatview/components/ChatViewTranscript.tsx @@ -290,7 +290,7 @@ export const ChatViewTranscript = memo(function ChatViewTranscript({ transcript, {pinnedAnnouncement.content}
- 由 {pinnedAnnouncement.author ?? '系统'} 置顶 + {t('pinnedAnnouncement.meta', { author: pinnedAnnouncement.author ?? t('pinnedAnnouncement.systemAuthor') })} {pinnedAnnouncement.time && {pinnedAnnouncement.time}}
diff --git a/app/shared/src/chatview/components/CompactDivider.tsx b/app/shared/src/chatview/components/CompactDivider.tsx index 11f2ce870..a3471031d 100644 --- a/app/shared/src/chatview/components/CompactDivider.tsx +++ b/app/shared/src/chatview/components/CompactDivider.tsx @@ -5,6 +5,8 @@ ══════════════════════════════════════════════════════════════════════ */ import React, { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { CHATVIEW_I18N_NAMESPACE } from '../i18n/resources' interface CompactDividerProps { /** Compaction trigger, e.g. "auto" or "manual". */ @@ -20,9 +22,10 @@ function formatPreTokens(n: number): string { /** Render a compact-boundary divider between transcript message groups. */ export const CompactDivider = memo(function CompactDivider({ trigger, preTokens }: CompactDividerProps) { - const parts: string[] = ['上下文已压缩'] - if (trigger === 'auto') parts.push('(自动)') - if (preTokens != null) parts.push(`${formatPreTokens(preTokens)} tokens 前`) + const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE) + const parts: string[] = [t('compactDivider.label')] + if (trigger === 'auto') parts.push(t('compactDivider.auto')) + if (preTokens != null) parts.push(t('compactDivider.tokensAgo', { tokens: formatPreTokens(preTokens) })) const label = parts.join(' ') diff --git a/app/shared/src/chatview/i18n/resources.ts b/app/shared/src/chatview/i18n/resources.ts index 97cb258cc..8874b6829 100644 --- a/app/shared/src/chatview/i18n/resources.ts +++ b/app/shared/src/chatview/i18n/resources.ts @@ -140,6 +140,21 @@ export const chatviewResources = { 'inlineDelegation.status.done': '完成 ✓', 'inlineDelegation.status.failed': '失败 ✗', 'inlineDelegation.status.cancelled': '已取消', + // ── CompactDivider ── + 'compactDivider.label': '上下文已压缩', + 'compactDivider.auto': '(自动)', + 'compactDivider.tokensAgo': '压缩前 {{tokens}} tokens', + // ── AgentStreamingBar ── + 'agentStreaming.status.dispatching': '调度中…', + 'agentStreaming.status.thinking': '思考中…', + 'agentStreaming.status.streaming': '输出中…', + 'agentStreaming.status.done': '完成', + 'agentStreaming.status.failed': '失败', + 'agentStreaming.toolCalls': '{{count}} 次工具调用', + 'agentStreaming.multiAgent': '{{count}} 个 Agent 运行中', + // ── Pinned announcement ── + 'pinnedAnnouncement.meta': '由 {{author}} 置顶', + 'pinnedAnnouncement.systemAuthor': '系统', 'card.fail.retry': '重试', 'card.expand': '展开', 'card.collapse': '收起', @@ -718,6 +733,23 @@ export const chatviewResources = { 'inlineDelegation.status.done': 'Done ✓', 'inlineDelegation.status.failed': 'Failed ✗', 'inlineDelegation.status.cancelled': 'Cancelled', + // ── CompactDivider ── + 'compactDivider.label': 'Context compressed', + 'compactDivider.auto': '(auto)', + 'compactDivider.tokensAgo': '{{tokens}} tokens before compaction', + // ── AgentStreamingBar ── + 'agentStreaming.status.dispatching': 'Dispatching…', + 'agentStreaming.status.thinking': 'Thinking…', + 'agentStreaming.status.streaming': 'Streaming…', + 'agentStreaming.status.done': 'Done', + 'agentStreaming.status.failed': 'Failed', + 'agentStreaming.toolCalls_one': '{{count}} tool call', + 'agentStreaming.toolCalls_other': '{{count}} tool calls', + 'agentStreaming.multiAgent_one': '{{count}} agent running', + 'agentStreaming.multiAgent_other': '{{count}} agents running', + // ── Pinned announcement ── + 'pinnedAnnouncement.meta': 'Pinned by {{author}}', + 'pinnedAnnouncement.systemAuthor': 'System', 'card.fail.retry': 'Retry', 'card.expand': 'Expand', 'card.collapse': 'Collapse', diff --git a/app/shared/src/ui/AgentStreamingBar.test.tsx b/app/shared/src/ui/AgentStreamingBar.test.tsx index b03546953..45f3d57c9 100644 --- a/app/shared/src/ui/AgentStreamingBar.test.tsx +++ b/app/shared/src/ui/AgentStreamingBar.test.tsx @@ -1,8 +1,17 @@ import React from 'react'; -import { describe, it, expect, afterEach } from 'vitest'; +import { describe, it, expect, afterEach, afterAll, beforeAll } from 'vitest'; import { render } from '@testing-library/react'; import { AgentStreamingBar } from './AgentStreamingBar'; import { getAgentActivityStore } from '../transcript/agentActivity'; +import { useTestI18nLanguage, TEST_I18N_DEFAULT_LNG } from '../testing/i18n'; + +beforeAll(async () => { + await useTestI18nLanguage('zh'); +}); + +afterAll(async () => { + await useTestI18nLanguage(TEST_I18N_DEFAULT_LNG); +}); afterEach(() => { getAgentActivityStore().reset(); diff --git a/app/shared/src/ui/AgentStreamingBar.tsx b/app/shared/src/ui/AgentStreamingBar.tsx index 680da2dcf..161ff2657 100644 --- a/app/shared/src/ui/AgentStreamingBar.tsx +++ b/app/shared/src/ui/AgentStreamingBar.tsx @@ -1,9 +1,11 @@ import React, { useSyncExternalStore } from 'react'; +import { useTranslation } from 'react-i18next'; import { getAgentActivityStore, type AgentActivitySnapshot, type AgentActivityStatus, } from '../transcript/agentActivity'; +import { CHATVIEW_I18N_NAMESPACE } from '../chatview/i18n/resources'; import styles from './AgentStreamingBar.module.css'; // ── Status icons ────────────────────────────────────────────────────────── @@ -16,6 +18,14 @@ const STATUS_ICON: Record = { failed: '\u{274C}', }; +const STATUS_LABEL_KEY: Record = { + dispatching: 'agentStreaming.status.dispatching', + thinking: 'agentStreaming.status.thinking', + streaming: 'agentStreaming.status.streaming', + done: 'agentStreaming.status.done', + failed: 'agentStreaming.status.failed', +}; + // ── Store binding ───────────────────────────────────────────────────────── const emptySnapshot: AgentActivitySnapshot = { activeAgents: [] }; @@ -44,6 +54,7 @@ export interface AgentStreamingBarProps { * Hidden when no agents are active. Uses `useSyncExternalStore` for reactivity. */ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React.ReactElement | null { + const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE); const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); const agents = snapshot.activeAgents; @@ -58,7 +69,7 @@ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React. // No real progress percentage exists for agent runs, so surface the one // quantitative signal we do have: the number of tool calls. const totalToolCalls = agents.reduce((sum, a) => sum + (a.toolCalls ?? 0), 0); - const toolCallsLabel = totalToolCalls > 0 ? ` · ${totalToolCalls} 次工具调用` : ''; + const toolCallsLabel = totalToolCalls > 0 ? ` · ${t('agentStreaming.toolCalls', { count: totalToolCalls })}` : ''; return (
@@ -67,14 +78,14 @@ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React. {agents[0]!.name} - {statusLabel(agents[0]!.status)} + {t(STATUS_LABEL_KEY[agents[0]!.status])} {toolCallsLabel}
) : (
- {activeCount} 个 Agent 运行中{toolCallsLabel} + {t('agentStreaming.multiAgent', { count: activeCount })}{toolCallsLabel} {agents.map((a) => STATUS_ICON[a.status]).join(' ')} @@ -86,16 +97,6 @@ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React. // ── Helpers ─────────────────────────────────────────────────────────────── -function statusLabel(status: AgentActivityStatus): string { - switch (status) { - case 'dispatching': return '调度中…'; - case 'thinking': return '思考中…'; - case 'streaming': return '输出中…'; - case 'done': return '完成'; - case 'failed': return '失败'; - } -} - function statusClassName(status: AgentActivityStatus): string { switch (status) { case 'streaming': return styles.statusStreaming ?? '';