diff --git a/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts b/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts new file mode 100644 index 0000000000..afe208a05f --- /dev/null +++ b/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { getPlanModeCopy } from '../../renderer/locales/plan-mode-copy.js'; + +test('localizes Plan Mode chrome and abandon confirmation without rewriting plan content', () => { + const zh = getPlanModeCopy('zh'); + const en = getPlanModeCopy('en'); + + assert.equal(zh.proposal.statuses.approved, '已批准'); + assert.equal(en.proposal.statuses.approved, 'Approved'); + assert.equal(zh.execution.stepCount(2, 3), '2/3 步'); + assert.equal(en.execution.stepCount(1, 1), '1/1 step'); + assert.deepEqual( + { + title: en.abandonConfirmation.title, + description: en.abandonConfirmation.description('Release plan'), + confirm: en.abandonConfirmation.confirm, + }, + { + title: 'Abandon this plan?', + description: 'The execution record for “Release plan” will remain, but it cannot be resumed.', + confirm: 'Abandon plan', + }, + ); +}); diff --git a/apps/desktop/src/renderer/locales/plan-mode-copy.ts b/apps/desktop/src/renderer/locales/plan-mode-copy.ts new file mode 100644 index 0000000000..f13d70641b --- /dev/null +++ b/apps/desktop/src/renderer/locales/plan-mode-copy.ts @@ -0,0 +1,76 @@ +import type { PlanExecutionStep, PlanProposal, UiCatalog, UiLocale } from '@maka/core'; + +export interface PlanModeCopy { + readonly abandonConfirmation: { + readonly title: string; + description(title: string): string; + readonly confirm: string; + readonly cancel: string; + }; + readonly proposal: { + readonly aria: string; + readonly kicker: string; + readonly revision: string; + readonly steps: string; + readonly risks: string; + readonly revise: string; + readonly execute: string; + readonly statuses: Record; + }; + readonly execution: { + readonly aria: string; + readonly interrupted: string; + readonly running: string; + readonly approvedPlan: string; + stepCount(completed: number, total: number): string; + readonly resume: string; + readonly abandon: string; + readonly stepStatuses: Record; + }; +} + +const COPY = { + zh: { + abandonConfirmation: { + title: '放弃这个计划?', + description: (title) => `“${title}”的执行记录会保留,但之后不能继续恢复。`, + confirm: '放弃计划', + cancel: '取消', + }, + proposal: { + aria: '计划方案', kicker: '计划方案', revision: '修订版', steps: '执行步骤', + risks: '风险', revise: '继续修改', execute: '执行计划', + statuses: { pending_approval: '等待确认', approved: '已批准', stale: '已过期' }, + }, + execution: { + aria: '计划执行状态', interrupted: '计划已中断', running: '正在执行计划', + approvedPlan: '已批准计划', stepCount: (completed, total) => `${completed}/${total} 步`, + resume: '恢复执行', abandon: '放弃计划', + stepStatuses: { pending: '未开始', in_progress: '正在执行', completed: '已完成', skipped: '已跳过' }, + }, + }, + en: { + abandonConfirmation: { + title: 'Abandon this plan?', + description: (title) => `The execution record for “${title}” will remain, but it cannot be resumed.`, + confirm: 'Abandon plan', + cancel: 'Cancel', + }, + proposal: { + aria: 'Plan proposal', kicker: 'Plan proposal', revision: 'Revision', steps: 'Steps', + risks: 'Risks', revise: 'Request changes', execute: 'Execute plan', + statuses: { pending_approval: 'Waiting for approval', approved: 'Approved', stale: 'Outdated' }, + }, + execution: { + aria: 'Plan execution status', interrupted: 'Plan interrupted', running: 'Executing plan', + approvedPlan: 'Approved plan', + stepCount: (completed, total) => `${completed}/${total} ${total === 1 ? 'step' : 'steps'}`, + resume: 'Resume', abandon: 'Abandon plan', + stepStatuses: { pending: 'Not started', in_progress: 'In progress', completed: 'Completed', skipped: 'Skipped' }, + }, + }, +} satisfies UiCatalog; + +export function getPlanModeCopy(locale: UiLocale): PlanModeCopy { + return COPY[locale]; +} diff --git a/apps/desktop/src/renderer/plan-mode-panel.tsx b/apps/desktop/src/renderer/plan-mode-panel.tsx index 8258082fd2..42c10d472c 100644 --- a/apps/desktop/src/renderer/plan-mode-panel.tsx +++ b/apps/desktop/src/renderer/plan-mode-panel.tsx @@ -8,7 +8,8 @@ import type { } from '@maka/core'; import { Banner } from '@astryxdesign/core/Banner'; import { Collapsible } from '@astryxdesign/core/Collapsible'; -import { Badge, type BadgeVariant, Button as UiButton, useToast } from '@maka/ui'; +import { Badge, type BadgeVariant, Button as UiButton, useToast, useUiLocale } from '@maka/ui'; +import { getPlanModeCopy, type PlanModeCopy } from './locales/plan-mode-copy.js'; export interface PlanModeState { state: PlanSessionState | undefined; @@ -22,6 +23,7 @@ export interface PlanModeState { export function usePlanModeState(session: SessionSummary | undefined): PlanModeState { const toastApi = useToast(); + const copy = getPlanModeCopy(useUiLocale()); const [state, setState] = useState(); const [pending, setPending] = useState(false); const [error, setError] = useState(); @@ -134,17 +136,17 @@ export function usePlanModeState(session: SessionSummary | undefined): PlanModeS const abandon = useCallback(async (executionId: string, title: string): Promise => { if (!session) return; const confirmed = await toastApi.confirm({ - title: '放弃这个计划?', - description: `“${title}”的执行记录会保留,但之后不能继续恢复。`, - confirmLabel: '放弃计划', - cancelLabel: '取消', + title: copy.abandonConfirmation.title, + description: copy.abandonConfirmation.description(title), + confirmLabel: copy.abandonConfirmation.confirm, + cancelLabel: copy.abandonConfirmation.cancel, destructive: true, }); if (!confirmed) return; await run(async () => { await window.maka.sessions.abandonPlanExecution(session.id, executionId); }); - }, [run, session?.id, toastApi]); + }, [copy, run, session?.id, toastApi]); return { state, pending, error, requestRevision, approve, resume, abandon }; } @@ -154,16 +156,17 @@ export function PlanProposalCard(props: { planMode: PlanModeState; }): JSX.Element { const { proposal, planMode } = props; + const copy = getPlanModeCopy(useUiLocale()).proposal; const reviewable = proposal.status === 'pending_approval' && planMode.state?.latestProposalId === proposal.proposalId; return ( -
+
- 计划方案 + {copy.kicker} {proposal.title}
@@ -171,19 +174,19 @@ export function PlanProposalCard(props: { className="plan-proposal-revision" label={ <> - Revision {proposal.revision} + {copy.revision} {proposal.revision} } />
{proposal.overview &&

{proposal.overview}

}
-

执行步骤

+

{copy.steps}

    {proposal.steps.map((step, index) => (
  1. @@ -198,7 +201,7 @@ export function PlanProposalCard(props: {
{proposal.risks && proposal.risks.length > 0 && (
-

风险

+

{copy.risks}

    {proposal.risks.map((risk, index) =>
  • {risk}
  • )}
@@ -211,14 +214,14 @@ export function PlanProposalCard(props: { size="sm" isDisabled={planMode.pending} onClick={() => void planMode.requestRevision(proposal.proposalId)} - label="继续修改" + label={copy.revise} /> void planMode.approve(proposal)} - label="执行计划" + label={copy.execute} />
)} @@ -234,6 +237,7 @@ export function PlanExecutionPanel(props: { planMode: PlanModeState; }): JSX.Element | null { const { planMode } = props; + const copy = getPlanModeCopy(useUiLocale()).execution; const [expanded, setExpanded] = useState(false); const detailsId = useId(); const active = planMode.state?.executions.find( @@ -256,7 +260,7 @@ export function PlanExecutionPanel(props: { ).length; return ( -
+
- {execution.status === 'interrupted' ? '计划已中断' : '正在执行计划'} - {proposal?.title ?? '已批准计划'} + {execution.status === 'interrupted' ? copy.interrupted : copy.running} + {proposal?.title ?? copy.approvedPlan}
- {completedCount}/{execution.steps.length} 步 + {copy.stepCount(completedCount, execution.steps.length)}
)} @@ -281,8 +285,8 @@ export function PlanExecutionPanel(props: { className="plan-execution-step-marker" data-status={step.status} role="img" - aria-label={executionStepStatusLabel(step.status)} - title={executionStepStatusLabel(step.status)} + aria-label={executionStepStatusLabel(step.status, copy)} + title={executionStepStatusLabel(step.status, copy)} > {executionStepMark(step.status)} @@ -297,7 +301,7 @@ export function PlanExecutionPanel(props: { size="sm" isDisabled={planMode.pending} onClick={() => void planMode.resume(execution.executionId)} - label="恢复执行" + label={copy.resume} /> void planMode.abandon( execution.executionId, - proposal?.title ?? '已批准计划', + proposal?.title ?? copy.approvedPlan, )} - label="放弃计划" + label={copy.abandon} /> )} @@ -328,10 +332,11 @@ function isPlanToolResult(event: SessionEvent): boolean { || kind === 'plan_execution_cancelled'; } -function proposalStatusLabel(status: PlanProposal['status']): string { - if (status === 'approved') return '已批准'; - if (status === 'stale') return '已过期'; - return '等待确认'; +function proposalStatusLabel( + status: PlanProposal['status'], + copy: PlanModeCopy['proposal'], +): string { + return copy.statuses[status]; } /* #1879: the status pill is an Astryx `Badge`. Only `approved` is a semantic @@ -344,11 +349,11 @@ function proposalStatusVariant(status: PlanProposal['status']): BadgeVariant { return status === 'approved' ? 'green' : 'neutral'; } -function executionStepStatusLabel(status: PlanExecutionStep['status']): string { - if (status === 'completed') return '已完成'; - if (status === 'in_progress') return '正在执行'; - if (status === 'skipped') return '已跳过'; - return '未开始'; +function executionStepStatusLabel( + status: PlanExecutionStep['status'], + copy: PlanModeCopy['execution'], +): string { + return copy.stepStatuses[status]; } function executionStepMark(status: PlanExecutionStep['status']): string {