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.
{STATUS_ICON[agents[0]!.status]}
{agents[0]!.name}
- {statusLabel(agents[0]!.status)}
+ {t(STATUS_LABEL_KEY[agents[0]!.status])}
{toolCallsLabel}
) : (
{'\u{1F916}'}
- {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 ?? '';