diff --git a/docs/assets/demo_termflow.gif b/docs/assets/demo_termflow.gif index 29f8e3a4..e1b28f74 100644 Binary files a/docs/assets/demo_termflow.gif and b/docs/assets/demo_termflow.gif differ diff --git a/packages/client-ui/src/App.test.ts b/packages/client-ui/src/App.test.ts index 17f96328..f1f54da5 100644 --- a/packages/client-ui/src/App.test.ts +++ b/packages/client-ui/src/App.test.ts @@ -183,4 +183,41 @@ describe('shared application routes', () => { expect(wrapper.find('[data-action="logout"]').exists()).toBe(true) wrapper.unmount() }) + + it('toggles UI language between Chinese and English via header locale switcher', async () => { + const router = createRouter({ history: createMemoryHistory(), routes: clientRoutes }) + await router.push('/') + await router.isReady() + const clientUi = createClientUi(createFakeRuntime()) + await clientUi.session.loginWithToken('test-only-token') + const wrapper = mount(App, { global: { plugins: [router, clientUi] } }) + await flushPromises() + + const toggleBtn = wrapper.get('[data-action="toggle-locale"]') + expect(toggleBtn.text()).toContain('EN') + expect(wrapper.get('.side-nav a[href="/"]').text()).toBe('控制中心') + expect(wrapper.get('.side-nav a[href="/computers"]').text()).toBe('电脑管理') + expect(wrapper.get('.side-nav a[href="/settings"]').text()).toBe('设置') + expect(wrapper.get('h1').text()).toBe('控制中心') + + await toggleBtn.trigger('click') + await flushPromises() + + expect(window.localStorage.getItem('termflow_lang')).toBe('en') + expect(toggleBtn.text()).toContain('中文') + expect(wrapper.get('.side-nav a[href="/"]').text()).toBe('Dashboard') + expect(wrapper.get('.side-nav a[href="/computers"]').text()).toBe('Computers') + expect(wrapper.get('.side-nav a[href="/settings"]').text()).toBe('Settings') + expect(wrapper.get('h1').text()).toBe('Dashboard') + expect(wrapper.get('[data-action="logout"]').attributes('aria-label')).toBe('Sign Out') + + await toggleBtn.trigger('click') + await flushPromises() + + expect(window.localStorage.getItem('termflow_lang')).toBe('zh') + expect(toggleBtn.text()).toContain('EN') + expect(wrapper.get('.side-nav a[href="/"]').text()).toBe('控制中心') + expect(wrapper.get('h1').text()).toBe('控制中心') + wrapper.unmount() + }) }) diff --git a/packages/client-ui/src/App.vue b/packages/client-ui/src/App.vue index e2b3a45f..724c8e1a 100644 --- a/packages/client-ui/src/App.vue +++ b/packages/client-ui/src/App.vue @@ -1,34 +1,45 @@ diff --git a/packages/client-ui/src/components/agent/AgentApprovalPanel.vue b/packages/client-ui/src/components/agent/AgentApprovalPanel.vue index bd737ea6..60546445 100644 --- a/packages/client-ui/src/components/agent/AgentApprovalPanel.vue +++ b/packages/client-ui/src/components/agent/AgentApprovalPanel.vue @@ -1,7 +1,7 @@ @@ -83,8 +83,11 @@ diff --git a/packages/client-ui/src/components/agent/AgentToolActivity.vue b/packages/client-ui/src/components/agent/AgentToolActivity.vue index 94e223ff..7223fbc0 100644 --- a/packages/client-ui/src/components/agent/AgentToolActivity.vue +++ b/packages/client-ui/src/components/agent/AgentToolActivity.vue @@ -37,17 +37,19 @@ import { computed, ref, useId } from 'vue' import { ChevronDown, ChevronRight } from '@lucide/vue' import { stripAnsiOsc, type AgentToolCallState } from '@termflow/client-core' +import { useI18n } from '../../i18n' import AgentCollapsibleOutput from './AgentCollapsibleOutput.vue' +const { t } = useI18n() const props = defineProps<{ /** Tool call record, exactly as tracked by the M6b history reducer. */ call: AgentToolCallState }>() -const STATUS_LABELS: Record = { - running: '运行中', - completed: '已完成', - failed: '执行失败', +const STATUS_LABEL_KEYS: Record = { + running: 'agent.chat.toolStatus.running', + completed: 'agent.chat.toolStatus.completed', + failed: 'agent.chat.toolStatus.failedExec', } const expanded = ref(false) @@ -58,8 +60,8 @@ const hasSummary = computed(() => props.call.summary !== null) // name carries a doubled ``termflow_`` prefix; show the bare tool name. const displayName = computed(() => { const bare = props.call.toolName.replace(/^termflow_termflow_/, '').replace(/^termflow_/, '') - return bare === '' ? '工具调用' : bare + return bare === '' ? t('agent.chat.toolCall') : bare }) -const statusLabel = computed(() => STATUS_LABELS[props.call.status]) +const statusLabel = computed(() => t(STATUS_LABEL_KEYS[props.call.status])) const summaryText = computed(() => stripAnsiOsc(props.call.summary ?? '')) diff --git a/packages/client-ui/src/components/agent/TerminalAgentPanel.vue b/packages/client-ui/src/components/agent/TerminalAgentPanel.vue index de330911..911d719c 100644 --- a/packages/client-ui/src/components/agent/TerminalAgentPanel.vue +++ b/packages/client-ui/src/components/agent/TerminalAgentPanel.vue @@ -13,14 +13,14 @@ :style="mobilePage ? undefined : panelStyle" >
-
+
@@ -87,8 +87,8 @@ type="button" class="icon-button icon-only" data-action="close-agent" - aria-label="关闭 Agent" - title="关闭 Agent" + :aria-label="t('agent.panel.closeAria')" + :title="t('agent.panel.closeAria')" @click="emit('close')" >