diff --git a/app/shared/src/chatview/i18n/resources.ts b/app/shared/src/chatview/i18n/resources.ts index 2ac34cf67..d089e18d5 100644 --- a/app/shared/src/chatview/i18n/resources.ts +++ b/app/shared/src/chatview/i18n/resources.ts @@ -848,6 +848,29 @@ export const chatviewResources = { 'oidc.loginSuccess.heading': '登录成功', 'oidc.loginSuccess.body': '登录流程已完成。', 'oidc.loginSuccess.closeHint': '您可以关闭此页面并返回 AgentHub 桌面应用。', + + // ── Shared UI preview surfaces (#2154 UIUX lane i18n pass) ── + 'preview.parsing': '正在解析文档...', + 'preview.parsingSlideshow': '正在解析演示文稿...', + 'preview.parsingTable': '正在解析表格...', + 'preview.retry': '重试', + 'preview.blankSlide': '空白幻灯片', + 'preview.emptySheet': '空工作表', + 'filePreview.imageViaFileUrl': '图片内容将通过文件 URL 加载', + 'inspector.runtimeEvidence': '运行证据', + 'addContact.qrTitle': 'TokenDance 企业二维码', + 'addContact.qrDescription': '对方扫码后可申请加入企业,管理员确认后出现在组织内联系人。', + 'addContact.inviteLink': '邀请链接', + 'addContact.copyLink': '复制链接', + 'addContact.inviteLinkDescription': '适合发给已在 TokenDance 协作空间内的同事。', + 'addContact.inviteCodeHint': '企业邀请码 24 小时内有效,可通过 IM 或邮件发送。', + 'addContact.copyInviteCode': '复制邀请码', + 'addContact.phone': '手机号', + 'addContact.phonePlaceholder': '输入手机号', + 'addContact.remark': '备注', + 'addContact.sendInvite': '发送邀请', + 'addContact.title': '添加联系人', + 'addContact.subtitle': '邀请企业成员加入 TokenDance,或添加外部联系人用于项目协作。', }, en: { @@ -1681,5 +1704,27 @@ export const chatviewResources = { 'oidc.loginSuccess.heading': 'Login Successful', 'oidc.loginSuccess.body': 'The login flow has completed.', 'oidc.loginSuccess.closeHint': 'You may close this page and return to the AgentHub desktop app.', + // ── Shared UI preview surfaces (#2154 UIUX lane i18n pass) ── + 'preview.parsing': 'Parsing document...', + 'preview.parsingSlideshow': 'Parsing slideshow...', + 'preview.parsingTable': 'Parsing spreadsheet...', + 'preview.retry': 'Retry', + 'preview.blankSlide': 'Blank slide', + 'preview.emptySheet': 'Empty sheet', + 'filePreview.imageViaFileUrl': 'Image content will load via file URL', + 'inspector.runtimeEvidence': 'Runtime evidence', + 'addContact.qrTitle': 'TokenDance enterprise QR code', + 'addContact.qrDescription': 'Scan to apply to join the enterprise; an admin confirms membership into your org contacts.', + 'addContact.inviteLink': 'Invite link', + 'addContact.copyLink': 'Copy link', + 'addContact.inviteLinkDescription': 'Best for colleagues already in a TokenDance workspace.', + 'addContact.inviteCodeHint': 'Enterprise invite codes are valid for 24 hours and can be sent via IM or email.', + 'addContact.copyInviteCode': 'Copy invite code', + 'addContact.phone': 'Phone number', + 'addContact.phonePlaceholder': 'Enter phone number', + 'addContact.remark': 'Remark', + 'addContact.sendInvite': 'Send invite', + 'addContact.title': 'Add contact', + 'addContact.subtitle': 'Invite teammates to TokenDance, or add external contacts for project collaboration.', }, } diff --git a/app/shared/src/i18n/workbench.ts b/app/shared/src/i18n/workbench.ts index 61118e814..9b0246978 100644 --- a/app/shared/src/i18n/workbench.ts +++ b/app/shared/src/i18n/workbench.ts @@ -157,6 +157,10 @@ export const sharedWorkbenchResources = { }, }, settings: { + close: '关闭', + currentSpace: '当前空间', + searchPlaceholder: '搜索设置项', + statusPreview: '状态组件预览', nav: { appearance: '外观', appearanceDesc: '主题、语言和界面风格', @@ -209,10 +213,22 @@ export const sharedWorkbenchResources = { loadFailed: '设置加载失败', }, projects: { + view: '视图', + announcement: '项目公告', + editAnnouncement: '编辑', + edit: '编辑项目', + projectName: '项目名称', + projectDescription: '项目描述', + themeColor: '主题色', + cancel: '取消', + saving: '保存中', + activeRuns: '活跃运行', + completed: '已完成', + queue: '等待队列', + artifactsPreview: '项目产物预览', nav: { all: '全部项目', running: '运行中', completed: '已完成', archived: '已归档' }, tab: { overview: '概览', settings: '设置', members: '成员' }, newProject: '新建项目', - edit: '编辑项目', projectRuns: '项目运行', loading: '正在加载项目…', loadMoreError: '加载更多项目失败:{{message}}', @@ -250,6 +266,16 @@ export const sharedWorkbenchResources = { }, }, contacts: { + manage: '管理', + recentContacts: '最近联系人', + tokenDanceGroups: 'TokenDance 群组', + openGroupChat: '打开群聊', + enter: '进入', + loadMore: '加载更多', + filter: '筛选', + newTicket: '新建工单', + add: '添加联系人', + new: { title: '新的联系人', subtitle: '搜索用户、发送好友请求、处理收到的请求。' }, error: { title: '通讯录加载失败', description: '请稍后重试;如持续失败请检查 Hub 连接。', @@ -278,6 +304,17 @@ export const sharedWorkbenchResources = { }, }, docs: { + home: '主页', + create: '新建', + deleteConfirm: '确认删除', + cancel: '取消', + colTitle: '标题', + colLocation: '位置', + colOwner: '所有者', + colOpenedAt: '打开时间', + lightPreview: '轻量文档预览', + myLibrary: '我的文档库', + loading: '加载中…', tab: { recent: '最近访问', mine: '归我所有', shared: '与我共享', starred: '收藏' }, search: { placeholder: '搜索云文档' }, newDoc: '新建文档', @@ -300,6 +337,7 @@ export const sharedWorkbenchResources = { }, }, agents: { + allowConfirmDeny: '允许 / 需确认 / 禁止', nav: { installed: '已安装', market: 'Agent 市场', @@ -590,6 +628,30 @@ export const sharedWorkbenchResources = { }, }, tasks: { + navTasks: '任务', + taskCenter: '跨项目任务中心;项目页只展示当前项目内任务和运行。', + quickAccess: '快速访问', + newGroup: '新建分组', + currentSelection: '当前选中', + statusFilter: '推进状态', + assignedToMe: '指派给我', + groupByProject: '按项目分组', + viewAssigneeTasks: '看负责人任务', + edit: '编辑', + delete: '删除', + save: '保存', + cancel: '取消', + taskTitle: '任务标题', + project: '所属项目', + assignee: '负责人', + startAt: '开始时间', + dueAt: '截止时间', + creator: '创建人', + importTasks: '导入任务', + exportView: '导出当前视图', + manageFields: '管理任务字段', + filter: '筛选', + loadMore: '加载更多', nav: { all: '全部任务', assigned: '分配给我', created: '我创建的', watching: '我关注的' }, view: { list: '列表', board: '看板', timeline: '时间线' }, newTask: '新建任务', @@ -841,6 +903,10 @@ export const sharedWorkbenchResources = { }, }, settings: { + close: 'Close', + currentSpace: 'Current space', + searchPlaceholder: 'Search settings', + statusPreview: 'Status component preview', nav: { appearance: 'Appearance', appearanceDesc: 'Theme, language and interface style', @@ -893,10 +959,22 @@ export const sharedWorkbenchResources = { loadFailed: 'Settings load failed', }, projects: { + view: 'View', + announcement: 'Project announcement', + editAnnouncement: 'Edit', + edit: 'Edit project', + projectName: 'Project name', + projectDescription: 'Project description', + themeColor: 'Theme color', + cancel: 'Cancel', + saving: 'Saving', + activeRuns: 'Active runs', + completed: 'Completed', + queue: 'Queue', + artifactsPreview: 'Project artifacts preview', nav: { all: 'All projects', running: 'Running', completed: 'Completed', archived: 'Archived' }, tab: { overview: 'Overview', settings: 'Settings', members: 'Members' }, newProject: 'New project', - edit: 'Edit project', projectRuns: 'Project runs', loading: 'Loading projects…', loadMoreError: 'Failed to load more projects: {{message}}', @@ -934,6 +1012,16 @@ export const sharedWorkbenchResources = { }, }, contacts: { + manage: 'Manage', + recentContacts: 'Recent contacts', + tokenDanceGroups: 'TokenDance groups', + openGroupChat: 'Open group chat', + enter: 'Open', + loadMore: 'Load more', + filter: 'Filter', + newTicket: 'New ticket', + add: 'Add contact', + new: { title: 'New contacts', subtitle: 'Search users, send friend requests, and handle incoming requests.' }, error: { title: 'Failed to load contacts', description: 'Please retry later; check the Hub connection if this persists.', @@ -962,6 +1050,17 @@ export const sharedWorkbenchResources = { }, }, docs: { + home: 'Home', + create: 'New', + deleteConfirm: 'Confirm delete', + cancel: 'Cancel', + colTitle: 'Title', + colLocation: 'Location', + colOwner: 'Owner', + colOpenedAt: 'Opened', + lightPreview: 'Lightweight document preview', + myLibrary: 'My document library', + loading: 'Loading…', tab: { recent: 'Recent', mine: 'Mine', shared: 'Shared with me', starred: 'Starred' }, search: { placeholder: 'Search docs' }, newDoc: 'New document', @@ -984,6 +1083,7 @@ export const sharedWorkbenchResources = { }, }, agents: { + allowConfirmDeny: 'Allow / Confirm / Deny', nav: { installed: 'Installed', market: 'Agent marketplace', @@ -1274,6 +1374,30 @@ export const sharedWorkbenchResources = { }, }, tasks: { + navTasks: 'Tasks', + taskCenter: 'Cross-project task center; project pages show only that project\'s tasks and runs.', + quickAccess: 'Quick access', + newGroup: 'New group', + currentSelection: 'Current selection', + statusFilter: 'Progress status', + assignedToMe: 'Assigned to me', + groupByProject: 'Group by project', + viewAssigneeTasks: 'View assignee tasks', + edit: 'Edit', + delete: 'Delete', + save: 'Save', + cancel: 'Cancel', + taskTitle: 'Task title', + project: 'Project', + assignee: 'Assignee', + startAt: 'Start', + dueAt: 'Due', + creator: 'Creator', + importTasks: 'Import tasks', + exportView: 'Export current view', + manageFields: 'Manage task fields', + filter: 'Filter', + loadMore: 'Load more', nav: { all: 'All tasks', assigned: 'Assigned to me', created: 'Created by me', watching: 'Watching' }, view: { list: 'List', board: 'Board', timeline: 'Timeline' }, newTask: 'New task', diff --git a/app/shared/src/ui/DocxPreview.tsx b/app/shared/src/ui/DocxPreview.tsx index 965910db1..170cf38bc 100644 --- a/app/shared/src/ui/DocxPreview.tsx +++ b/app/shared/src/ui/DocxPreview.tsx @@ -119,7 +119,7 @@ export const DocxPreview: React.FC = ({ {loading && (
- 正在解析文档... + {t('preview.parsing')}
)} @@ -129,7 +129,7 @@ export const DocxPreview: React.FC = ({ {error}
)} diff --git a/app/shared/src/ui/SlideshowPreview.test.tsx b/app/shared/src/ui/SlideshowPreview.test.tsx index d4f2649f0..96dc61fc5 100644 --- a/app/shared/src/ui/SlideshowPreview.test.tsx +++ b/app/shared/src/ui/SlideshowPreview.test.tsx @@ -1,4 +1,5 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { useTestI18nLanguage } from '../testing/i18n'; import { render, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; import React from 'react'; @@ -25,6 +26,10 @@ import React from 'react'; `findByText` would throw on a multiple-match. ═══════════════════════════════════════════════════════════════════════ */ +beforeAll(async () => { + await useTestI18nLanguage('zh'); +}); + const mockSlideContents: Record = { 'ppt/slides/slide1.xml': 'First Slide Heading TextFirst body content', @@ -125,15 +130,15 @@ describe('SlideshowPreview', () => { const onClose = vi.fn(); const { findByRole } = renderSlideshow({ onClose }); // The shared test i18n instance runs in key-echo mode, so - // t('aria.closePreview') returns 'aria.closePreview'. - const closeButton = await findByRole('button', { name: 'aria.closePreview' }); + // Real zh copy for the close-preview control (#2154 UIUX lane i18n pass). + const closeButton = await findByRole('button', { name: '关闭预览' }); expect(closeButton).toBeInTheDocument(); }); it('shows prev/next buttons with key-echo aria-labels', async () => { const { findByRole } = renderSlideshow(); - const prevButton = await findByRole('button', { name: 'aria.previousImage' }); - const nextButton = await findByRole('button', { name: 'aria.nextImage' }); + const prevButton = await findByRole('button', { name: '上一张' }); + const nextButton = await findByRole('button', { name: '下一张' }); expect(prevButton).toBeInTheDocument(); expect(nextButton).toBeInTheDocument(); }); @@ -141,7 +146,7 @@ describe('SlideshowPreview', () => { it('calls onClose when close button is clicked', async () => { const onClose = vi.fn(); const { findByRole } = renderSlideshow({ onClose }); - const closeButton = await findByRole('button', { name: 'aria.closePreview' }); + const closeButton = await findByRole('button', { name: '关闭预览' }); fireEvent.click(closeButton); expect(onClose).toHaveBeenCalledTimes(1); }); @@ -150,7 +155,7 @@ describe('SlideshowPreview', () => { const { findByRole, findByText, getByText } = renderSlideshow(); // Wait for slide 1 to be rendered before interacting. await findByText('First Slide Heading Text'); - const nextButton = await findByRole('button', { name: 'aria.nextImage' }); + const nextButton = await findByRole('button', { name: '下一张' }); fireEvent.click(nextButton); // Slide 2 heading now renders in the slide canvas. expect(await findByText('Second Slide Heading Text')).toBeInTheDocument(); @@ -159,7 +164,7 @@ describe('SlideshowPreview', () => { it('disables prev button on the first slide', async () => { const { findByRole } = renderSlideshow(); - const prevButton = await findByRole('button', { name: 'aria.previousImage' }); + const prevButton = await findByRole('button', { name: '上一张' }); expect(prevButton).toBeDisabled(); }); diff --git a/app/shared/src/ui/SlideshowPreview.tsx b/app/shared/src/ui/SlideshowPreview.tsx index 6759f98d1..aca767c92 100644 --- a/app/shared/src/ui/SlideshowPreview.tsx +++ b/app/shared/src/ui/SlideshowPreview.tsx @@ -257,7 +257,7 @@ export const SlideshowPreview: React.FC = ({ {loading && (
- 正在解析演示文稿... + {t('preview.parsingSlideshow')}
)} @@ -268,7 +268,7 @@ export const SlideshowPreview: React.FC = ({ {error}
)} @@ -301,7 +301,7 @@ export const SlideshowPreview: React.FC = ({ )} {currentSlideData.text.length === 0 && currentSlideData.images.length === 0 && ( -
空白幻灯片
+
{t('preview.blankSlide')}
)} diff --git a/app/shared/src/ui/TablePreview.test.tsx b/app/shared/src/ui/TablePreview.test.tsx index d3d1cee91..7c46f368e 100644 --- a/app/shared/src/ui/TablePreview.test.tsx +++ b/app/shared/src/ui/TablePreview.test.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, beforeAll } from 'vitest'; +import { useTestI18nLanguage } from '../testing/i18n'; import * as XLSX from 'xlsx'; import { MAX_PREVIEW_FILE_BYTES, TablePreview } from './TablePreview'; +beforeAll(async () => { + await useTestI18nLanguage('zh'); +}); + /* Size guard tests — defense-in-depth retained after the xlsx migration to the official SheetJS source 0.20.3 (issue #1358). Oversized payloads must hit the existing error UI without ever reaching XLSX.read; normal-sized diff --git a/app/shared/src/ui/TablePreview.tsx b/app/shared/src/ui/TablePreview.tsx index f95ca536a..6397528a3 100644 --- a/app/shared/src/ui/TablePreview.tsx +++ b/app/shared/src/ui/TablePreview.tsx @@ -372,7 +372,7 @@ export const TablePreview: React.FC = ({ {loading && (
- 正在解析表格... + {t('preview.parsingTable')}
)} @@ -383,7 +383,7 @@ export const TablePreview: React.FC = ({ {error}
)} @@ -448,7 +448,7 @@ export const TablePreview: React.FC = ({ /* #1851 review: multi-sheet empty workbooks still mount the controlled tabpanel so the tabs' aria-controls resolves. */
- 空工作表 + {t('preview.emptySheet')}
)} diff --git a/app/workbench/src/inspector/FilePreviewParts.tsx b/app/workbench/src/inspector/FilePreviewParts.tsx index eb0fc66e3..230edbe78 100644 --- a/app/workbench/src/inspector/FilePreviewParts.tsx +++ b/app/workbench/src/inspector/FilePreviewParts.tsx @@ -90,6 +90,7 @@ export function HtmlPreview({ content }: { content: string }): React.ReactElemen } export function ImagePreview({ filename }: { filename: string }): React.ReactElement { + const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE); // Image preview — the actual image URL would be resolved by the parent. // Shows a placeholder when only filename is available. return ( @@ -112,7 +113,7 @@ export function ImagePreview({ filename }: { filename: string }): React.ReactEle }}> 图片预览: {filename} - 图片内容将通过文件 URL 加载 + {t('filePreview.imageViaFileUrl')} ); diff --git a/app/workbench/src/inspector/RuntimeEvidencePanel.tsx b/app/workbench/src/inspector/RuntimeEvidencePanel.tsx index 0e525d593..1f1435757 100644 --- a/app/workbench/src/inspector/RuntimeEvidencePanel.tsx +++ b/app/workbench/src/inspector/RuntimeEvidencePanel.tsx @@ -57,7 +57,7 @@ export function RuntimeEvidencePanel({ return (
- 运行证据 + {t('inspector.runtimeEvidence')} {evidenceModel.runLabel}
diff --git a/app/workbench/src/pages/ContactsPage.test.tsx b/app/workbench/src/pages/ContactsPage.test.tsx index abb37f089..b87420003 100644 --- a/app/workbench/src/pages/ContactsPage.test.tsx +++ b/app/workbench/src/pages/ContactsPage.test.tsx @@ -1,8 +1,13 @@ import React from 'react'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { useTestI18nLanguage } from '@shared/testing/i18n'; import { fireEvent, render, screen } from '../__tests__/setup'; import { ContactsPage } from './ContactsPage'; +beforeAll(async () => { + await useTestI18nLanguage('zh'); +}); + describe('ContactsPage empty states', () => { it('uses shared EmptyState for the new-contacts primary empty path', () => { render( diff --git a/app/workbench/src/pages/SettingsPage.test.tsx b/app/workbench/src/pages/SettingsPage.test.tsx index 9c9adce69..da0ded943 100644 --- a/app/workbench/src/pages/SettingsPage.test.tsx +++ b/app/workbench/src/pages/SettingsPage.test.tsx @@ -1,8 +1,13 @@ import React from 'react'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { useTestI18nLanguage } from '@shared/testing/i18n'; import { render, screen } from '../__tests__/setup'; import { SettingsPage, type SettingsPageProps } from './SettingsPage'; +beforeAll(async () => { + await useTestI18nLanguage('zh'); +}); + const BASE_PROPS: SettingsPageProps = { activePane: 'appearance', spaceTitle: 'AgentHub Desktop', @@ -64,7 +69,7 @@ describe('SettingsPage load/error UX', () => { />, ); - expect(screen.getByRole('alert', { name: 'settings.loadFailed' })).toBeInTheDocument(); + expect(screen.getByRole('alert', { name: '设置加载失败' })).toBeInTheDocument(); expect(screen.getByText('backend down')).toBeInTheDocument(); screen.getByRole('button', { name: '重试加载' }).click(); diff --git a/app/workbench/src/pages/agents/AgentEditItemParts.tsx b/app/workbench/src/pages/agents/AgentEditItemParts.tsx index 3d68fe659..1f5d69803 100644 --- a/app/workbench/src/pages/agents/AgentEditItemParts.tsx +++ b/app/workbench/src/pages/agents/AgentEditItemParts.tsx @@ -137,7 +137,7 @@ export const AgentToolPermissions: React.FC<{

{t('agents.detail.tools')}

{/* Legend mirrors the ToolPermission enum identifiers rendered below (data-plane direct display; cross-surface enum decision #2015). */} - 允许 / 需确认 / 禁止 + {t('agents.allowConfirmDeny')}
{allTools.map((tool) => (
diff --git a/app/workbench/src/pages/contacts/AddContactModal.tsx b/app/workbench/src/pages/contacts/AddContactModal.tsx index a801680c6..b324bfe78 100644 --- a/app/workbench/src/pages/contacts/AddContactModal.tsx +++ b/app/workbench/src/pages/contacts/AddContactModal.tsx @@ -41,9 +41,9 @@ function QRPanel() { TD
-

TokenDance 企业二维码

+

{t('addContact.qrTitle')}

- 对方扫码后可申请加入企业,管理员确认后出现在组织内联系人。 + {t('addContact.qrDescription')}

{expireLabel} @@ -51,10 +51,11 @@ function QRPanel() { } function LinkPanel({ onCopy }: { onCopy?: (() => void) | undefined }) { + const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE); const linkInputId = React.useId(); return (
- +
void) | undefined }) { value="https://hub.example.com/invite/TD-2026" />

- 适合发给已在 TokenDance 协作空间内的同事。 + {t('addContact.inviteLinkDescription')}

); } function CodePanel({ onCopy }: { onCopy?: (() => void) | undefined }) { + const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE); return (
TD-86K2-2026

- 企业邀请码 24 小时内有效,可通过 IM 或邮件发送。 + {t('addContact.inviteCodeHint')}

); @@ -92,6 +94,7 @@ function PhonePanel({ }: { onSend?: ((countryCode: string, phone: string, note: string) => void) | undefined; }) { + const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE); const [countryCode, setCountryCode] = useState('+86'); const [phone, setPhone] = useState(''); const [note, setNote] = useState(''); @@ -111,7 +114,7 @@ function PhonePanel({ handleSend(); }} > - +
setNote(e.target.value)} /> ); @@ -244,10 +247,10 @@ export function AddContactModal({

- 添加联系人 + {t('addContact.title')}

- 邀请企业成员加入 TokenDance,或添加外部联系人用于项目协作。 + {t('addContact.subtitle')}

diff --git a/app/workbench/src/pages/contacts/ContactMainParts.tsx b/app/workbench/src/pages/contacts/ContactMainParts.tsx index c3c63ee87..336339237 100644 --- a/app/workbench/src/pages/contacts/ContactMainParts.tsx +++ b/app/workbench/src/pages/contacts/ContactMainParts.tsx @@ -67,6 +67,7 @@ export function ContactListPage({ onMemberClick?: ((member: ContactMember) => void) | undefined; onAvatarClick?: ((member: ContactMember, anchor: HTMLElement) => void) | undefined; }): React.ReactElement { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); // ── Infinite-scroll sentinel (T14 pattern; wired to the mock data-layer // cursor pagination, #1510) ── const sentinelRef = useRef(null); @@ -115,7 +116,7 @@ export function ContactListPage({ IntersectionObserver) ── */} {hasMore && !loadingMore ? ( ) : null}
-
TokenDance 群组
+
{t('contacts.tokenDanceGroups')}
{orgInitials}
{orgName}
@@ -82,7 +82,7 @@ export function ContactNav({ ))} -
最近联系人
+
{t('contacts.recentContacts')}
{recentShortcuts.map((name) => (
{name} diff --git a/app/workbench/src/pages/contacts/ContactRows.tsx b/app/workbench/src/pages/contacts/ContactRows.tsx index 3bb6481d9..c127b97ad 100644 --- a/app/workbench/src/pages/contacts/ContactRows.tsx +++ b/app/workbench/src/pages/contacts/ContactRows.tsx @@ -1,6 +1,7 @@ import React, { useCallback } from 'react'; import { appDateLocaleTag } from '@shared/i18n/locale'; -import { getI18n } from 'react-i18next'; +import { getI18n, useTranslation } from 'react-i18next'; +import { SHARED_WORKBENCH_I18N_NAMESPACE } from '@shared/i18n'; import styles from '../ContactsPage.module.css'; import { capabilityColor } from './shared'; import type { @@ -170,6 +171,7 @@ export function GroupRow({ onAvatarClick?: ((group: ContactGroup, anchor: HTMLElement) => void) | undefined; avatarExpanded?: boolean; }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); const handleClick = useCallback(() => { onClick?.(group); }, [group, onClick]); @@ -207,7 +209,7 @@ export function GroupRow({ {group.latestMessage} - 打开群聊 + {t('contacts.openGroupChat')} ); } @@ -223,6 +225,7 @@ export function ServiceCardRow({ onAvatarClick?: ((desk: ServiceDesk, anchor: HTMLElement) => void) | undefined; avatarExpanded?: boolean; }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); const handleClick = useCallback(() => { onClick?.(desk); }, [desk, onClick]); @@ -259,7 +262,7 @@ export function ServiceCardRow({ {desk.name} {desk.description}
- 进入 + {t('contacts.enter')} ); } diff --git a/app/workbench/src/pages/docs/DocMain.tsx b/app/workbench/src/pages/docs/DocMain.tsx index b32940c88..ab288a751 100644 --- a/app/workbench/src/pages/docs/DocMain.tsx +++ b/app/workbench/src/pages/docs/DocMain.tsx @@ -55,7 +55,7 @@ export function DocMain({
{/* Head */}
-

主页

+

{t('docs.home')}

- + + ) : onDelete ? ( @@ -112,10 +112,10 @@ export function DocTable({ return (
- 标题 - 位置 - 所有者 - 打开时间 + {t('docs.colTitle')} + {t('docs.colLocation')} + {t('docs.colOwner')} + {t('docs.colOpenedAt')}
{documentsLoading && rows.length === 0 ? ( @@ -209,6 +209,7 @@ export function DocPreviewPanel({ activePreview: WorkbenchDocumentPreview; onClosePreview?: (() => void) | undefined; }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); return (
@@ -216,7 +217,7 @@ export function DocPreviewPanel({ {activePreview.sourceLabel} {activePreview.name}
- 轻量文档预览 + {t('docs.lightPreview')}
))} -
我的文档库
+
{t('docs.myLibrary')}
{shortcuts.map((name) => (
void) | undefined; }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); return (
- 项目公告 + {t('projects.announcement')}

{text}

@@ -29,7 +32,7 @@ export function ProjectAnnouncement({ className={styles.announcementEditBtn} onClick={onEdit} > - 编辑 + {t('projects.editAnnouncement')} ) : null}
diff --git a/app/workbench/src/pages/projects/ProjectChromeViews.tsx b/app/workbench/src/pages/projects/ProjectChromeViews.tsx index a26be8f62..54281bd7c 100644 --- a/app/workbench/src/pages/projects/ProjectChromeViews.tsx +++ b/app/workbench/src/pages/projects/ProjectChromeViews.tsx @@ -132,7 +132,7 @@ export function ProjectEditor({ >
- 主题色 + {t('projects.themeColor')}
{FOLDER_THEME_COLORS.map((color) => { const meta = FOLDER_THEME_COLOR_META[color]; @@ -182,10 +182,10 @@ export function ProjectEditor({ {error ?
{error}
: null}
diff --git a/app/workbench/src/pages/projects/ProjectNav.tsx b/app/workbench/src/pages/projects/ProjectNav.tsx index f004a0d21..2f655362d 100644 --- a/app/workbench/src/pages/projects/ProjectNav.tsx +++ b/app/workbench/src/pages/projects/ProjectNav.tsx @@ -128,7 +128,7 @@ export function ProjectNav({ onSelect={onProjectSelect} /> ))} -
视图
+
{t('projects.view')}
{/* Infinite-scroll sentinel: triggers loadMore when within 200px of viewport. */}
- 活跃运行 + {t('projects.activeRuns')} {running}
- 已完成 + {t('projects.completed')} {completed}
- 等待队列 + {t('projects.queue')} {waiting}
@@ -184,6 +184,7 @@ export function ProjectPreviewPanel({ preview?: WorkbenchDocumentPreview | null | undefined; onClosePreview?: (() => void) | undefined; }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); if (!preview) return null; return ( @@ -193,7 +194,7 @@ export function ProjectPreviewPanel({ {preview.sourceLabel} {preview.name}
- 项目产物预览 + {t('projects.artifactsPreview')}
- 关闭 + {tw('settings.close')} ), } diff --git a/app/workbench/src/pages/settings/SettingsNav.tsx b/app/workbench/src/pages/settings/SettingsNav.tsx index 18087b940..1f9d13d86 100644 --- a/app/workbench/src/pages/settings/SettingsNav.tsx +++ b/app/workbench/src/pages/settings/SettingsNav.tsx @@ -6,6 +6,8 @@ ═══════════════════════════════════════════════════════════════════════ */ import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SHARED_WORKBENCH_I18N_NAMESPACE } from '@shared/i18n'; import styles from '../SettingsPage.module.css'; import { NavGlyph, SettingsScopeRow } from './shared'; import { NAV_ITEMS } from './types'; @@ -26,13 +28,14 @@ export function SettingsNav({ spaceMeta, currentUserDisplayName, }: SettingsNavProps): React.ReactElement { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); return ( ); diff --git a/app/workbench/src/pages/tasks/TaskTableParts.tsx b/app/workbench/src/pages/tasks/TaskTableParts.tsx index 4b962b2bb..b4b857035 100644 --- a/app/workbench/src/pages/tasks/TaskTableParts.tsx +++ b/app/workbench/src/pages/tasks/TaskTableParts.tsx @@ -86,7 +86,7 @@ export function TaskSelectionStrip({ aria-label={taskSelectionAriaLabel(task.title)} >
- 当前选中 + {t('tasks.currentSelection')} {task.title} {task.status}
@@ -120,12 +120,12 @@ export function TaskSelectionStrip({ )} - - - - - - + + + + + +
); @@ -167,6 +167,7 @@ function TaskEditFields({ onSaveEdit?: (() => void) | undefined; onCancelEdit?: (() => void) | undefined; }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); return ( <> {TASK_EDIT_FIELD_CONFIGS.map((field) => ( @@ -180,8 +181,8 @@ function TaskEditFields({ ))} - - + + ); @@ -262,14 +263,15 @@ export function TaskRow({ } export function TaskTableHead({ showCreatorColumn }: { showCreatorColumn: boolean }) { + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); return (
- 任务标题 - 所属项目 - 负责人 - 开始时间 - 截止时间 - {showCreatorColumn && 创建人} + {t('tasks.taskTitle')} + {t('tasks.project')} + {t('tasks.assignee')} + {t('tasks.startAt')} + {t('tasks.dueAt')} + {showCreatorColumn && {t('tasks.creator')}}
); } diff --git a/app/workbench/src/pages/tasks/shared.tsx b/app/workbench/src/pages/tasks/shared.tsx index af2db4140..a853b5914 100644 --- a/app/workbench/src/pages/tasks/shared.tsx +++ b/app/workbench/src/pages/tasks/shared.tsx @@ -14,6 +14,7 @@ import styles from '../TasksPage.module.css'; import type { TasksPane, ViewMode } from './types'; import { useTranslation } from 'react-i18next'; import { CHATVIEW_I18N_NAMESPACE } from '@shared/chatview/i18n/resources'; +import { SHARED_WORKBENCH_I18N_NAMESPACE } from '@shared/i18n'; // ── Nav definitions ── @@ -87,22 +88,23 @@ export function StatCard({ label, value }: { label: string; value: number }) { } export function TaskNavMenu({ open }: { open: boolean }) { - const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE); + const { t } = useTranslation(SHARED_WORKBENCH_I18N_NAMESPACE); + const { t: tc } = useTranslation(CHATVIEW_I18N_NAMESPACE); if (!open) return null; return ( -
+
);