From 550fce3689c931b0a5c2926d0e60b49483d506a3 Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Tue, 1 Sep 2026 19:50:02 -0700 Subject: [PATCH 1/3] chore(desktop): move inline bilingual copy into catalogs Typed locale catalogs are the mandated home for user-visible copy; inline locale ternaries scattered through components bypass that and make copy audits miss strings. This is the mechanical consolidation wave: zh and en strings are moved verbatim into UiCatalog-shaped tables with no behavior or string changes. Generated-by: Claude Code --- apps/desktop/renderer-architecture.json | 31 + .../__tests__/agent-graph-panel-copy.test.ts | 2 +- ...traditional-chinese-peer-mesh-copy.test.ts | 14 +- apps/desktop/src/main/notifications-policy.ts | 32 +- .../src/renderer/agent-graph-panel.tsx | 219 +----- .../model/task-readiness-notice.ts | 50 +- .../features/runtime-host-management/index.ts | 1 - .../ui/runtime-host-peer-mesh-dialog.tsx | 524 +------------- .../src/renderer/locales/agent-graph-copy.ts | 235 +++++++ .../src/renderer/locales/peer-mesh-copy.ts | 646 ++++++++++++++++++ .../locales/settings-projects-copy.ts | 24 + .../renderer/locales/task-readiness-copy.ts | 76 +++ apps/desktop/src/renderer/mcp-catalog.ts | 79 ++- .../renderer/settings/provider-add-form.tsx | 1 + .../runtime-host-profiles-section.tsx | 42 +- 15 files changed, 1132 insertions(+), 844 deletions(-) create mode 100644 apps/desktop/src/renderer/locales/agent-graph-copy.ts create mode 100644 apps/desktop/src/renderer/locales/peer-mesh-copy.ts create mode 100644 apps/desktop/src/renderer/locales/task-readiness-copy.ts diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 4570aa0895..a235122e61 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -62,12 +62,14 @@ "src/renderer/live-turn-reconciler.tsx", "src/renderer/live-turn-snapshot.ts", "src/renderer/local-memory-digest.ts", + "src/renderer/locales/agent-graph-copy.ts", "src/renderer/locales/artifact-copy.ts", "src/renderer/locales/browser-copy.ts", "src/renderer/locales/conversation-copy.ts", "src/renderer/locales/external-session-import-copy.ts", "src/renderer/locales/mcp-copy.ts", "src/renderer/locales/onboarding-copy.ts", + "src/renderer/locales/peer-mesh-copy.ts", "src/renderer/locales/permission-center-copy.ts", "src/renderer/locales/plan-mode-copy.ts", "src/renderer/locales/session-collaboration-copy.ts", @@ -87,6 +89,7 @@ "src/renderer/locales/settings-web-search-copy.ts", "src/renderer/locales/shell-copy.ts", "src/renderer/locales/shell-remaining-copy.ts", + "src/renderer/locales/task-readiness-copy.ts", "src/renderer/main.tsx", "src/renderer/mcp-brand-contrast.ts", "src/renderer/mcp-brand-marks.tsx", @@ -1067,6 +1070,7 @@ "dependencyPaths": { "./agent-graph-panel-visibility.js": 1, "./agent-graph-refresh.js": 1, + "./locales/agent-graph-copy.js": 1, "@astryxdesign/core/Banner": 1, "@astryxdesign/core/Button": 1, "@astryxdesign/core/EmptyState": 1, @@ -1532,6 +1536,15 @@ "actionFactories": [], "dependencyPaths": {} }, + "src/renderer/locales/agent-graph-copy.ts": { + "bridgePaths": {}, + "environmentCapabilities": {}, + "hookCalls": {}, + "lifecycleMethods": {}, + "unresolvedDependencies": 0, + "actionFactories": [], + "dependencyPaths": {} + }, "src/renderer/locales/artifact-copy.ts": { "bridgePaths": {}, "environmentCapabilities": {}, @@ -1586,6 +1599,15 @@ "actionFactories": [], "dependencyPaths": {} }, + "src/renderer/locales/peer-mesh-copy.ts": { + "bridgePaths": {}, + "environmentCapabilities": {}, + "hookCalls": {}, + "lifecycleMethods": {}, + "unresolvedDependencies": 0, + "actionFactories": [], + "dependencyPaths": {} + }, "src/renderer/locales/permission-center-copy.ts": { "bridgePaths": {}, "environmentCapabilities": {}, @@ -1759,6 +1781,15 @@ "actionFactories": [], "dependencyPaths": {} }, + "src/renderer/locales/task-readiness-copy.ts": { + "bridgePaths": {}, + "environmentCapabilities": {}, + "hookCalls": {}, + "lifecycleMethods": {}, + "unresolvedDependencies": 0, + "actionFactories": [], + "dependencyPaths": {} + }, "src/renderer/mcp-brand-contrast.ts": { "bridgePaths": {}, "environmentCapabilities": {}, diff --git a/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts b/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts index 0ef254444c..16d5369040 100644 --- a/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts +++ b/apps/desktop/src/main/__tests__/agent-graph-panel-copy.test.ts @@ -19,7 +19,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { getAgentGraphPanelCopy } from '../../renderer/agent-graph-panel.js'; +import { getAgentGraphPanelCopy } from '../../renderer/locales/agent-graph-copy.js'; test('Traditional Chinese Agent Graph copy does not use Simplified fallbacks', () => { const copy = getAgentGraphPanelCopy('zh-TW'); diff --git a/apps/desktop/src/main/__tests__/traditional-chinese-peer-mesh-copy.test.ts b/apps/desktop/src/main/__tests__/traditional-chinese-peer-mesh-copy.test.ts index 67af56d965..5de2b34cdb 100644 --- a/apps/desktop/src/main/__tests__/traditional-chinese-peer-mesh-copy.test.ts +++ b/apps/desktop/src/main/__tests__/traditional-chinese-peer-mesh-copy.test.ts @@ -19,21 +19,23 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { getRuntimeHostPeerMeshCopy } from '../../renderer/features/runtime-host-management/index.js'; +import { getPeerMeshCopy } from '../../renderer/locales/peer-mesh-copy.js'; +import { getSettingsProjectsCopy } from '../../renderer/locales/settings-projects-copy.js'; import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js'; import { getProviderSettingsCopy } from '../../renderer/features/connection-settings/index.js'; import { settingsTestResultMessage } from '../../renderer/locales/settings-test-result-copy.js'; test('Traditional Chinese Peer Mesh copy does not use the Simplified Chinese branch', () => { - const copy = getRuntimeHostPeerMeshCopy('zh-TW'); + const copy = getPeerMeshCopy('zh-TW'); + const runtimeHost = getSettingsProjectsCopy('zh-TW').runtimeHost; assert.equal(copy.experimental, '實驗性'); assert.equal(copy.invalidResult, 'Peer Mesh 回傳了無效結果'); assert.equal(copy.copyPeerId('peer-1'), '複製完整 Peer ID:peer-1'); - assert.equal(copy.peerIdCopyFailed, '無法複製 Peer ID'); - assert.equal(copy.peerPathDirect, '直接連線'); - assert.equal(copy.peerPathTransit, '成員轉送'); - assert.equal(copy.peerPathOther, '其他'); + assert.equal(runtimeHost.peerIdCopyFailed, '無法複製 Peer ID'); + assert.equal(runtimeHost.peerPathDirect, '直接連線'); + assert.equal(runtimeHost.peerPathTransit, '成員轉送'); + assert.equal(runtimeHost.peerPathTransportOther, '其他'); assert.equal(copy.joinHint, '貼上另一個 Peer 產生的一次性邀請碼。'); }); diff --git a/apps/desktop/src/main/notifications-policy.ts b/apps/desktop/src/main/notifications-policy.ts index 8795aa00d9..32da6335e4 100644 --- a/apps/desktop/src/main/notifications-policy.ts +++ b/apps/desktop/src/main/notifications-policy.ts @@ -17,7 +17,7 @@ * under the License. */ -import type { UiLocale } from '@maka/core/ui-locale'; +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; /** * Pure decision + copy helpers for desktop run-completion notifications. @@ -77,24 +77,26 @@ export interface RunNotificationCopy { * could not supply a session name / reply preview (e.g. an untitled * session or a tool-only turn with no assistant text). */ +const RUN_NOTIFICATION_COPY = { + 'zh-CN': { + errored: { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' }, + completed: { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' }, + }, + 'zh-TW': { + errored: { title: '任務發生錯誤', body: '本次回答未能完成,按一下以檢視詳細資料。' }, + completed: { title: '回答已產生', body: 'Maka 已完成本次回答,按一下以檢視。' }, + }, + en: { + errored: { title: 'Conversation error', body: 'This response did not finish. Click to view details.' }, + completed: { title: 'Response ready', body: 'Maka finished this response. Click to view it.' }, + }, +} satisfies UiCatalog>; + export function runNotificationCopy( kind: RunNotificationKind, locale: UiLocale, ): RunNotificationCopy { - if (locale === 'en') { - return kind === 'errored' - ? { title: 'Conversation error', body: 'This response did not finish. Click to view details.' } - : { title: 'Response ready', body: 'Maka finished this response. Click to view it.' }; - } - if (locale === 'zh-TW') { - return kind === 'errored' - ? { title: '任務發生錯誤', body: '本次回答未能完成,按一下以檢視詳細資料。' } - : { title: '回答已產生', body: 'Maka 已完成本次回答,按一下以檢視。' }; - } - if (kind === 'errored') { - return { title: '任务出错', body: '本轮回答未能完成,点击查看详情。' }; - } - return { title: '回答已生成', body: 'Maka 已完成本轮回答,点击查看。' }; + return RUN_NOTIFICATION_COPY[locale][kind]; } /** Renderer-supplied content for a finished turn. Both fields are diff --git a/apps/desktop/src/renderer/agent-graph-panel.tsx b/apps/desktop/src/renderer/agent-graph-panel.tsx index 48a3bbcda6..89c9d70f07 100644 --- a/apps/desktop/src/renderer/agent-graph-panel.tsx +++ b/apps/desktop/src/renderer/agent-graph-panel.tsx @@ -19,10 +19,7 @@ import { useEffect, useId, useMemo, useRef, useState, type JSX } from 'react'; import type { UiLocale } from '@maka/core/ui-locale'; -import type { - AgentGraphClientOperator, - AgentGraphClientSnapshot, -} from '@maka/runtime/stream-graph-read-model'; +import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model'; import type { AgentGraphEpochDirectory } from '@maka/runtime-host/client'; import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol'; import { IconButton, Selector, type SelectorOptionType } from '@maka/ui'; @@ -42,6 +39,9 @@ import { createAgentGraphRefreshScheduler, type AgentGraphRefreshScheduler, } from './agent-graph-refresh.js'; +import { getAgentGraphPanelCopy } from './locales/agent-graph-copy.js'; + +export { getAgentGraphPanelCopy }; const noopAgentGraphRefreshScheduler: AgentGraphRefreshScheduler = { requestRefresh() {}, @@ -50,177 +50,6 @@ const noopAgentGraphRefreshScheduler: AgentGraphRefreshScheduler = { dispose() {}, }; -type GraphPanelCopy = { - title: string; - loading: string; - retry: string; - collapse: string; - expand: string; - dismiss: string; - stop: string; - stopping: string; - stopFailed: string; - loadFailed: string; - openSession: string; - operators: string; - selectedResults: string; - epoch: string; - currentEpoch: string; - historicalEpoch: string; - cappedEpochs(count: number): string; - noOperators: string; - hiddenOperators(count: number): string; - progress(settled: number, total: number, hasOmitted: boolean): string; - status(status: AgentGraphClientSnapshot['status']): string; - operatorStatus(status: AgentGraphClientOperator['status']): string; - wait(operator: AgentGraphClientOperator): string | undefined; -}; - -export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy { - if (locale === 'zh-CN') { - return { - title: 'Agent Graph', - loading: '正在读取 Graph 状态…', - retry: '重试', - collapse: '收起 Agent Graph', - expand: '展开 Agent Graph', - dismiss: '关闭 Agent Graph', - stop: '停止 Graph', - stopping: '停止中…', - stopFailed: '停止 Graph 失败,请重试。', - loadFailed: 'Graph 状态刷新失败。', - openSession: '打开子任务', - operators: 'Operators', - selectedResults: '已选择结果', - epoch: 'Graph 运行轮次', - currentEpoch: '当前', - historicalEpoch: '历史记录(只读)', - cappedEpochs: (count) => `仅显示最近 ${count} 次运行`, - noOperators: '等待主 Agent 创建 operator…', - hiddenOperators: (count) => `另有 ${count} 个 operator`, - progress: (settled, total, hasOmitted) => - hasOmitted ? `可见 ${settled}/${total} 已结束` : `${settled}/${total} 已结束`, - status: (status) => - ({ - empty: '等待调度', - active: '运行中', - closing: '收尾中', - waiting: '等待中', - stopped: '已停止', - failed: '失败', - completed: '已完成', - })[status], - operatorStatus: (status) => - ({ - not_started: '未启动', - waiting: '等待', - runnable: '可运行', - running: '运行中', - blocked: '受阻', - completed: '完成', - failed: '失败', - aborted: '中止', - cancelled: '取消', - })[status], - wait: waitReasonZh, - }; - } - if (locale === 'zh-TW') { - return { - title: 'Agent Graph', - loading: '正在讀取 Graph 狀態…', - retry: '重試', - collapse: '收起 Agent Graph', - expand: '展開 Agent Graph', - dismiss: '關閉 Agent Graph', - stop: '停止 Graph', - stopping: '停止中…', - stopFailed: '停止 Graph 失敗,請重試。', - loadFailed: 'Graph 狀態重新整理失敗。', - openSession: '開啟子任務', - operators: 'Operators', - selectedResults: '已選取結果', - epoch: 'Graph 執行輪次', - currentEpoch: '目前', - historicalEpoch: '歷史記錄(唯讀)', - cappedEpochs: (count) => `僅顯示最近 ${count} 次執行`, - noOperators: '等待主 Agent 建立 operator…', - hiddenOperators: (count) => `另有 ${count} 個 operator`, - progress: (settled, total, hasOmitted) => - hasOmitted ? `可見項目中 ${settled}/${total} 已結束` : `${settled}/${total} 已結束`, - status: (status) => - ({ - empty: '等待排程', - active: '執行中', - closing: '收尾中', - waiting: '等待中', - stopped: '已停止', - failed: '失敗', - completed: '已完成', - })[status], - operatorStatus: (status) => - ({ - not_started: '尚未啟動', - waiting: '等待', - runnable: '可執行', - running: '執行中', - blocked: '受阻', - completed: '完成', - failed: '失敗', - aborted: '已中止', - cancelled: '已取消', - })[status], - wait: waitReasonZhTw, - }; - } - return { - title: 'Agent Graph', - loading: 'Loading graph state…', - retry: 'Retry', - collapse: 'Collapse Agent Graph', - expand: 'Expand Agent Graph', - dismiss: 'Dismiss Agent Graph', - stop: 'Stop graph', - stopping: 'Stopping…', - stopFailed: 'Could not stop the graph. Try again.', - loadFailed: 'Could not refresh graph state.', - openSession: 'Open child task', - operators: 'Operators', - selectedResults: 'Selected results', - epoch: 'Graph run', - currentEpoch: 'Current', - historicalEpoch: 'History (read-only)', - cappedEpochs: (count) => `Showing the newest ${count} runs`, - noOperators: 'Waiting for the main agent to create an operator…', - hiddenOperators: (count) => `${count} more operator${count === 1 ? '' : 's'}`, - progress: (settled, total, hasOmitted) => - hasOmitted ? `${settled}/${total} visible settled` : `${settled}/${total} settled`, - status: (status) => - ({ - empty: 'Awaiting schedule', - active: 'Running', - closing: 'Finishing', - waiting: 'Waiting', - stopped: 'Stopped', - failed: 'Failed', - completed: 'Completed', - })[status], - operatorStatus: (status) => - ({ - not_started: 'Not started', - waiting: 'Waiting', - runnable: 'Runnable', - running: 'Running', - blocked: 'Blocked', - completed: 'Completed', - failed: 'Failed', - aborted: 'Aborted', - cancelled: 'Cancelled', - })[status], - wait: waitReasonEn, - }; -} - export function AgentGraphPanel(props: { rootSessionId: string; enabled: boolean; @@ -586,43 +415,3 @@ function sameEpochPage( ); }); } - -function firstWait(operator: AgentGraphClientOperator) { - return operator.readiness.find((readiness) => readiness.status === 'waiting')?.waitingFor[0]; -} - -function waitReasonEn(operator: AgentGraphClientOperator): string | undefined { - const wait = firstWait(operator); - if (!wait) return undefined; - if (wait.kind === 'input_route') { - return `Waiting for input from ${wait.upstreamOperatorIds.join(', ')}`; - } - if (wait.kind === 'activation_missing') { - return `Waiting for ${wait.operatorId} activation`; - } - return `Waiting for ${wait.operatorId} to settle`; -} - -function waitReasonZh(operator: AgentGraphClientOperator): string | undefined { - const wait = firstWait(operator); - if (!wait) return undefined; - if (wait.kind === 'input_route') { - return `等待 ${wait.upstreamOperatorIds.join('、')} 的输入`; - } - if (wait.kind === 'activation_missing') { - return `等待 ${wait.operatorId} activation`; - } - return `等待 ${wait.operatorId} 结束`; -} - -function waitReasonZhTw(operator: AgentGraphClientOperator): string | undefined { - const wait = firstWait(operator); - if (!wait) return undefined; - if (wait.kind === 'input_route') { - return `等待 ${wait.upstreamOperatorIds.join('、')} 的輸入`; - } - if (wait.kind === 'activation_missing') { - return `等待 ${wait.operatorId} 啟用`; - } - return `等待 ${wait.operatorId} 結束`; -} diff --git a/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts b/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts index df90d2a57e..4a27c74ffe 100644 --- a/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts +++ b/apps/desktop/src/renderer/features/conversation/model/task-readiness-notice.ts @@ -22,6 +22,7 @@ import type { SessionSendProjection } from '@maka/core/session-send-projection'; import type { TaskSubmissionReadinessDimension, TaskSubmissionReadinessSnapshot } from '@maka/core/task-submission-readiness'; import type { UiLocale } from '@maka/core/ui-locale'; +import { getTaskReadinessCopy } from '../../../locales/task-readiness-copy.js'; /** Selects the stored model target for the renderer's readiness probe. */ export function resolveTaskReadinessModelTarget( @@ -85,57 +86,22 @@ function noticeForBlocker( blocker: TaskSubmissionReadinessDimension, locale: UiLocale, ): TaskReadinessNotice { + const copy = getTaskReadinessCopy(locale); if (blocker.id === 'runtime') { - if (locale === 'zh-CN') { - return { - tone: 'destructive', - title: 'Maka 运行服务暂时不可用。', - description: '任务尚未提交。重新检测运行服务后再试。', - actionLabel: '重新检测', - action: 'retry', - }; - } - if (locale === 'zh-TW') { - return { - tone: 'destructive', - title: 'Maka 執行服務暫時無法使用。', - description: '任務尚未送出。重新檢查執行服務後再試。', - actionLabel: '重新檢查', - action: 'retry', - }; - } return { tone: 'destructive', - title: 'The Maka runtime is unavailable.', - description: 'The task was not submitted. Check the runtime again before retrying.', - actionLabel: 'Check again', + title: copy.runtime.title, + description: copy.runtime.description, + actionLabel: copy.runtime.actionLabel, action: 'retry', }; } const action = blocker.repairTarget?.kind === 'workspace_picker' ? 'workspace_picker' : 'retry'; - if (locale === 'zh-CN') { - return { - tone: 'destructive', - title: '当前任务的工作区不可用。', - description: '原目录可能已移动、删除或无法访问。请选择可用工作区。', - actionLabel: action === 'workspace_picker' ? '选择工作区' : '重新检测', - action, - }; - } - if (locale === 'zh-TW') { - return { - tone: 'destructive', - title: '目前任務的工作區無法使用。', - description: '原始資料夾可能已移動、刪除或無法存取。請選擇可用的工作區。', - actionLabel: action === 'workspace_picker' ? '選擇工作區' : '重新檢查', - action, - }; - } return { tone: 'destructive', - title: 'This task workspace is unavailable.', - description: 'The folder may have moved, been deleted, or become inaccessible. Choose an available workspace.', - actionLabel: action === 'workspace_picker' ? 'Choose workspace' : 'Check again', + title: copy.workspace.title, + description: copy.workspace.description, + actionLabel: copy.workspace.actionLabel[action], action, }; } diff --git a/apps/desktop/src/renderer/features/runtime-host-management/index.ts b/apps/desktop/src/renderer/features/runtime-host-management/index.ts index 2e7cd2244e..a64ae63f53 100644 --- a/apps/desktop/src/renderer/features/runtime-host-management/index.ts +++ b/apps/desktop/src/renderer/features/runtime-host-management/index.ts @@ -18,7 +18,6 @@ */ export { - getRuntimeHostPeerMeshCopy, RuntimeHostPeerMeshDialog, } from './ui/runtime-host-peer-mesh-dialog.js'; export { PeerMeshPeerIdButton } from './ui/peer-mesh-peer-id-button.js'; diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx index ecd956369b..30d49263d7 100644 --- a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx +++ b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-peer-mesh-dialog.tsx @@ -25,7 +25,6 @@ import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; import { HStack } from '@astryxdesign/core/Stack'; import { Tooltip } from '@astryxdesign/core/Tooltip'; import type { PeerMeshProjection, PeerMeshQueryResult } from '@maka/runtime-host/protocol'; -import type { UiLocale } from '@maka/core/ui-locale'; import { decodeRuntimeHostWebRtcStunPolicy, type RuntimeHostWebRtcStunPolicy, @@ -56,6 +55,7 @@ import { RefreshCcw, Workflow, } from '@maka/ui/icons'; +import { getPeerMeshCopy, type PeerMeshCopy } from '../../../locales/peer-mesh-copy.js'; import { useRuntimeHostManagementServices } from '../services-context.js'; import { PeerMeshOperationOutcomeUnknownError, @@ -127,7 +127,7 @@ export function RuntimeHostPeerMeshDialog(props: { readonly onClose: () => void; }) { const locale = useUiLocale(); - const copy = getRuntimeHostPeerMeshCopy(locale); + const copy = getPeerMeshCopy(locale); const toast = useToast(); const services = useRuntimeHostManagementServices().peerMesh; const [snapshot, setSnapshot] = useState(); @@ -856,7 +856,7 @@ export function RuntimeHostPeerMeshDialog(props: { function PeerMeshAdvancedSettings(props: { readonly localPeerId?: string; readonly connectivity?: DesktopConnectivityControls; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly onCopyPeerId: (peerId: string) => void; }) { const { connectivity } = props; @@ -1008,7 +1008,7 @@ function PeerMeshAdvancedSettings(props: { function Overview(props: { readonly snapshot: PeerMeshQueryResult | undefined; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly working: boolean; readonly localPeerLabel: string; readonly onInvite: (meshId: string) => void; @@ -1233,7 +1233,7 @@ function Overview(props: { function UnavailableEndpoint(props: { readonly setup: ManagedHostPeerSetup; readonly working: boolean; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly onEnable: () => void; readonly onInspect?: () => void; readonly onRefresh: () => void; @@ -1316,7 +1316,7 @@ function UnavailableEndpoint(props: { function JoinView(props: { readonly value: string; readonly working: boolean; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly onChange: (value: string) => void; }) { return ( @@ -1348,7 +1348,7 @@ function JoinView(props: { function InvitationView(props: { readonly invitation: Extract; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; }) { return (
@@ -1399,7 +1399,7 @@ function InvitationView(props: { function MeshCard(props: { readonly mesh: PeerMeshProjection; readonly transit: PeerMeshQueryResult['transit']; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly working: boolean; readonly onInvite: () => void; readonly onRemove: (peerId: string) => void; @@ -1699,7 +1699,7 @@ function TransitMetric(props: { readonly label: string; readonly value: string } function PeerIdText(props: { readonly peerId: string; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly onCopy: (peerId: string) => void; }) { return ( @@ -1718,7 +1718,7 @@ function PeerIdText(props: { function MeshIdText(props: { readonly meshId: string; - readonly copy: ReturnType; + readonly copy: PeerMeshCopy; readonly onCopy: (meshId: string) => void; }) { return ( @@ -1803,507 +1803,3 @@ function fingerprint(meshId: string): string { function abbreviate(peerId: string): string { return peerId.length <= 22 ? peerId : `${peerId.slice(0, 11)}…${peerId.slice(-7)}`; } - -export function getRuntimeHostPeerMeshCopy(locale: UiLocale) { - return locale === 'zh-CN' - ? { - title: 'Peer Mesh', - experimental: '实验性', - failed: 'Peer Mesh 操作失败', - invalidResult: 'Peer Mesh 返回了无效结果', - unknownError: 'Peer Mesh 操作失败', - outcomeUnknown: 'Host 可能已完成此操作。已刷新当前状态,请确认后再重试。', - invitationOutcomeUnknown: - 'Host 可能已创建邀请码,但代码未能返回且无法恢复。它会自动过期;创建新邀请前请先检查待使用邀请数量。', - outcomeUnknownRefreshFailed: - 'Host 可能已完成此操作,但当前状态也未能刷新。请恢复连接并刷新后再重试。', - unavailable: '当前 endpoint 不支持 Peer Mesh', - loading: '正在读取 Mesh 状态…', - checkingPeerConnection: '正在检查此 Runtime Host 的 Peer 连接…', - peerConnectionDisabled: '此 Runtime Host 尚未开启 Peer 连接', - peerConnectionDisabledHint: - '开启后,此 Host 才能创建或加入 Mesh;现有 SSH 连接会继续保留。', - peerConnectionDisableProfileFirst: - '此 Host 的 Direct peer 连接正在使用中,请先在 Host 列表中将它停用。', - enablePeerConnection: '开启 Peer 连接', - peerConnectionStarting: 'Peer 连接已开启,Mesh endpoint 正在就绪', - peerConnectionStartingHint: '通常只需几秒;也可以立即重新检查。', - peerConnectionUpgradeRequired: '此 Runtime Host 版本尚不支持 Peer Mesh 管理', - peerConnectionUpgradeRequiredHint: '请先更新此 Host,再回来开启 Peer 连接。', - working: { - refresh: '正在刷新 Peer Mesh…', - create: '正在创建 Mesh…', - join: '正在加入 Mesh…', - invite: '正在准备邀请…', - 'add-host': '正在将 Runtime Host 加入 Mesh…', - 'enable-peer': '正在为 Runtime Host 开启 Peer 连接…', - update: '正在更新 Mesh…', - rename: '正在保存名称…', - }, - settling: '正在确认最终状态…', - endpoint: '管理对象', - desktopEndpoint: 'Desktop Client', - hostEndpoint: '本机 Runtime Host', - desktopEndpointHelp: '此 Client 用于连接 Mesh 中的 Runtime Host。', - hostEndpointHelp: '此 Host 加入后,其他成员才能连接本机分享的任务。', - advancedSettings: '高级设置', - technicalDetails: '身份与连接配置', - connectivityAutomatic: '自动连接(推荐)', - connectivityKnownRoutesOnly: '仅使用已知路径', - connectivityCustom: '自定义地址发现', - restartRequired: '需要重启', - adaptiveConnectivity: '自适应连接', - adaptiveConnectivityHelp: - 'Maka 会自动竞速可用的直连方式,并在获准时使用成员转发;这里不需要选择具体协议。', - connectivityPolicyLoading: '正在读取连接策略…', - connectivityPolicyLoadFailed: '无法读取连接策略', - connectivityPolicySaveFailed: '无法保存连接策略', - restoreDefaultConnectivityPolicy: '恢复默认设置', - connectivityPolicyRestartRequired: - '重启 Maka 后,已保存的连接策略变更会应用到新连接。', - publicAddressDiscovery: '公网地址发现', - publicStunDefault: '公共 STUN(推荐)', - publicStunDisabled: '不使用公共 STUN', - publicStunCustom: '自定义 STUN', - customStunUrls: 'STUN 地址', - customStunUrlsInvalid: - '请输入以逗号分隔的 stun:主机[:端口] 地址,最多 8 个。', - publicStunDefaultHelp: - '使用 Cloudflare 公共 STUN 尽力发现公网映射。它不承载 Maka 流量,但提供方可观察源 IP 和请求时间;Maka 不保证其可用性。', - publicStunDisabledHelp: - '仅尝试本地地址和其他已知直连路径;跨 NAT 的直连成功率可能降低。', - publicStunCustomHelp: - '使用逗号分隔的 stun: 地址。STUN 只发现网络地址,不承载 Session 内容。', - saveConnectivityPolicy: '保存更改', - peerId: 'Peer ID', - peerIdHelp: '此 endpoint 在 Mesh 中的技术身份;点击 ID 可复制完整值。', - meshId: 'Mesh ID', - meshIdHelp: '用于诊断和识别此 Mesh;点击 ID 可复制完整值。', - thisRuntimeHost: '本机 Runtime Host', - thisDesktop: '本机 Desktop', - peerPathDirect: '直连', - peerPathTransit: '成员转发', - peerPathOther: '其他', - displayName: '在 Mesh 中显示的名称', - meshDisplayName: 'Mesh 名称', - unnamedMesh: '未命名 Mesh', - rename: '修改名称', - renameMesh: '修改 Mesh 名称', - save: '保存', - peerIdCopied: 'Peer ID 已复制', - peerIdCopyFailed: '无法复制 Peer ID', - meshIdCopied: 'Mesh ID 已复制', - copyPeerId: (value: string) => `复制完整 Peer ID:${value}`, - copyMeshId: (value: string) => `复制完整 Mesh ID:${value}`, - empty: '建立你的第一个 Mesh', - emptyHint: '创建新 Mesh,或通过一次性邀请码加入。', - meshes: 'Mesh', - mesh: 'Mesh', - members: '成员', - activeMeshCount: (value: number) => `${value} 个使用中`, - showClosedMeshes: (value: number) => `显示已关闭(${value})`, - noActiveMeshes: '没有使用中的 Mesh', - noActiveMeshesHint: '已关闭的 Mesh 默认隐藏;可通过上方筛选查看。', - authority: '管理者', - member: '成员', - closed: '已关闭', - memberCount: (value: number) => `${value} 个成员`, - pending: (value: number) => `${value} 个待使用邀请`, - transit: '成员转发', - transitHelp: '允许此 Mesh 的成员通过本机建立连接;会使用本机带宽。', - transitToggle: '为此 Mesh 提供转发', - transitStatus: '成员转发状态', - transitLimitsLabel: '成员转发限制', - transitLimits: (value: PeerMeshQueryResult['transit']) => - value - ? `固定上限:每个成员 ${value.maxCircuitsPerPeer} 条连接,每条最长 ${formatHours(value.maxCircuitDurationSeconds)},最多 ${formatMebibytes(value.maxCircuitBytes)}。一次只能为一个 Mesh 开启。` - : '成员转发使用固定资源上限,一次只能为一个 Mesh 开启。', - allowedMembers: '允许成员', - reservations: 'Reservation', - circuits: '连接', - routeState: { - local: '本机', - connecting: '正在连接', - reachable: '可达', - reconnecting: '正在恢复连接', - needs_repair: '需要新邀请码修复', - }, - endpointKind: { - client: 'Client', - host: 'Runtime Host', - unknown: '未标识 Peer', - }, - endpointKindHelp: { - client: 'Client 是操作界面:它连接 Host、浏览任务并发起操作,本身不持有任务。', - host: 'Runtime Host 持有任务和运行状态,并执行经过授权的工作。', - unknown: '此 Peer 尚未报告它是 Client 还是 Runtime Host,通常来自旧版本。', - }, - joinTitle: '加入 Mesh', - joinHint: '粘贴另一个 Peer 生成的一次性邀请码。', - joinCode: '邀请码', - join: '加入', - joinMesh: '加入 Mesh', - invite: '邀请成员', - invitationTitle: '邀请成员', - invitationFor: (value: string) => `Mesh ${value}`, - invitationWarning: '该代码只能使用一次;获得代码的人可以让一个 peer 加入此 Mesh。', - invitationDirectOnly: - '尚未连接到协调节点。此邀请码只包含直接地址,跨 NAT 时可能无法连接。', - invitationExpires: (value: string) => `有效期至 ${value}`, - invitationCopied: '邀请码已复制', - copyInvitation: '复制邀请码', - create: '创建 Mesh', - refresh: '刷新', - back: '返回', - leave: '退出 Mesh', - closeMesh: '关闭 Mesh', - remove: '移除成员', - cancel: '取消', - closeConfirm: '关闭这个 Mesh?', - leaveConfirm: '退出这个 Mesh?', - removeConfirm: '移除这个成员?', - meshActions: 'Mesh 操作', - memberActions: (peerId: string) => `${peerId} 的操作`, - addLocalHost: '添加本机 Runtime Host', - localRuntimeHost: 'Runtime Host', - localHostMissing: '本机 Runtime Host 尚未加入', - localHostMissingHint: '加入后,其他成员才能通过此 Mesh 连接本机分享的任务。', - } - : locale === 'zh-TW' - ? { - title: 'Peer Mesh', - experimental: '實驗性', - failed: 'Peer Mesh 操作失敗', - invalidResult: 'Peer Mesh 回傳了無效結果', - unknownError: 'Peer Mesh 操作失敗', - outcomeUnknown: 'Host 可能已完成此操作。已重新整理目前狀態,請確認後再重試。', - invitationOutcomeUnknown: - 'Host 可能已建立邀請碼,但代碼未能回傳且無法復原。它會自動過期;建立新邀請前請先檢查待使用邀請數量。', - outcomeUnknownRefreshFailed: - 'Host 可能已完成此操作,但目前狀態也無法重新整理。請恢復連線並重新整理後再重試。', - unavailable: '目前 endpoint 不支援 Peer Mesh', - loading: '正在讀取 Mesh 狀態…', - checkingPeerConnection: '正在檢查此 Runtime Host 的 Peer 連線…', - peerConnectionDisabled: '此 Runtime Host 尚未開啟 Peer 連線', - peerConnectionDisabledHint: - '開啟後,此 Host 才能建立或加入 Mesh;現有 SSH 連線會繼續保留。', - peerConnectionDisableProfileFirst: - '此 Host 的 Direct peer 連線正在使用中,請先在 Host 列表中將它停用。', - enablePeerConnection: '開啟 Peer 連線', - peerConnectionStarting: 'Peer 連線已開啟,Mesh endpoint 正在就緒', - peerConnectionStartingHint: '通常只需幾秒;也可以立即重新檢查。', - peerConnectionUpgradeRequired: '此 Runtime Host 版本尚不支援 Peer Mesh 管理', - peerConnectionUpgradeRequiredHint: '請先更新此 Host,再回來開啟 Peer 連線。', - working: { - refresh: '正在重新整理 Peer Mesh…', - create: '正在建立 Mesh…', - join: '正在加入 Mesh…', - invite: '正在準備邀請…', - 'add-host': '正在將 Runtime Host 加入 Mesh…', - 'enable-peer': '正在為 Runtime Host 開啟 Peer 連線…', - update: '正在更新 Mesh…', - rename: '正在儲存名稱…', - }, - settling: '正在確認最終狀態…', - endpoint: '管理對象', - desktopEndpoint: 'Desktop Client', - hostEndpoint: '本機 Runtime Host', - desktopEndpointHelp: '此 Client 用於連線至 Mesh 中的 Runtime Host。', - hostEndpointHelp: '此 Host 加入後,其他成員才能連線至本機分享的任務。', - advancedSettings: '進階設定', - technicalDetails: '身分與連線設定', - connectivityAutomatic: '自動連線(建議)', - connectivityKnownRoutesOnly: '僅使用已知路徑', - connectivityCustom: '自訂位址探索', - restartRequired: '需要重新啟動', - adaptiveConnectivity: '自適應連線', - adaptiveConnectivityHelp: - 'Maka 會自動競速可用的直接連線方式,並在獲准時使用成員轉送;此處不需要選擇特定通訊協定。', - connectivityPolicyLoading: '正在讀取連線策略…', - connectivityPolicyLoadFailed: '無法讀取連線策略', - connectivityPolicySaveFailed: '無法儲存連線策略', - restoreDefaultConnectivityPolicy: '恢復預設設定', - connectivityPolicyRestartRequired: - '重新啟動 Maka 後,已儲存的連線策略變更會套用至新連線。', - publicAddressDiscovery: '公網位址探索', - publicStunDefault: '公共 STUN(建議)', - publicStunDisabled: '不使用公共 STUN', - publicStunCustom: '自訂 STUN', - customStunUrls: 'STUN 位址', - customStunUrlsInvalid: '請輸入以逗號分隔的 stun:主機[:連接埠] 位址,最多 8 個。', - publicStunDefaultHelp: - '使用 Cloudflare 公共 STUN 盡力探索公網對映。它不承載 Maka 流量,但提供者可觀察來源 IP 和請求時間;Maka 不保證其可用性。', - publicStunDisabledHelp: - '僅嘗試本機位址和其他已知直接連線路徑;跨 NAT 的直接連線成功率可能降低。', - publicStunCustomHelp: - '使用逗號分隔的 stun: 位址。STUN 只探索網路位址,不承載 Session 內容。', - saveConnectivityPolicy: '儲存變更', - peerId: 'Peer ID', - peerIdHelp: '此 endpoint 在 Mesh 中的技術身分;點選 ID 可複製完整值。', - meshId: 'Mesh ID', - meshIdHelp: '用於診斷和識別此 Mesh;點選 ID 可複製完整值。', - thisRuntimeHost: '本機 Runtime Host', - thisDesktop: '本機 Desktop', - peerPathDirect: '直接連線', - peerPathTransit: '成員轉送', - peerPathOther: '其他', - displayName: '在 Mesh 中顯示的名稱', - meshDisplayName: 'Mesh 名稱', - unnamedMesh: '未命名 Mesh', - rename: '修改名稱', - renameMesh: '修改 Mesh 名稱', - save: '儲存', - peerIdCopied: 'Peer ID 已複製', - peerIdCopyFailed: '無法複製 Peer ID', - meshIdCopied: 'Mesh ID 已複製', - copyPeerId: (value: string) => `複製完整 Peer ID:${value}`, - copyMeshId: (value: string) => `複製完整 Mesh ID:${value}`, - empty: '建立你的第一個 Mesh', - emptyHint: '建立新的 Mesh,或透過一次性邀請碼加入。', - meshes: 'Mesh', - mesh: 'Mesh', - members: '成員', - activeMeshCount: (value: number) => `${value} 個使用中`, - showClosedMeshes: (value: number) => `顯示已關閉(${value})`, - noActiveMeshes: '沒有使用中的 Mesh', - noActiveMeshesHint: '已關閉的 Mesh 預設隱藏;可透過上方篩選查看。', - authority: '管理者', - member: '成員', - closed: '已關閉', - memberCount: (value: number) => `${value} 個成員`, - pending: (value: number) => `${value} 個待使用邀請`, - transit: '成員轉送', - transitHelp: '允許此 Mesh 的成員透過本機建立連線;會使用本機頻寬。', - transitToggle: '為此 Mesh 提供轉送', - transitStatus: '成員轉送狀態', - transitLimitsLabel: '成員轉送限制', - transitLimits: (value: PeerMeshQueryResult['transit']) => - value - ? `固定上限:每位成員 ${value.maxCircuitsPerPeer} 條連線,每條最長 ${formatHours(value.maxCircuitDurationSeconds)},最多 ${formatMebibytes(value.maxCircuitBytes)}。一次只能為一個 Mesh 開啟。` - : '成員轉送使用固定資源上限,一次只能為一個 Mesh 開啟。', - allowedMembers: '允許成員', - reservations: 'Reservation', - circuits: '連線', - routeState: { - local: '本機', - route_available: '路徑可用', - coordination_only: '僅協調路徑', - stale: '路徑已過期', - unknown: '路徑未知', - }, - endpointKind: { - client: 'Client', - host: 'Runtime Host', - unknown: '未識別 Peer', - }, - endpointKindHelp: { - client: 'Client 是操作介面:它連線至 Host、瀏覽任務並發起操作,本身不持有任務。', - host: 'Runtime Host 持有任務和執行狀態,並執行經過授權的工作。', - unknown: '此 Peer 尚未回報它是 Client 或 Runtime Host,通常來自舊版本。', - }, - joinTitle: '加入 Mesh', - joinHint: '貼上另一個 Peer 產生的一次性邀請碼。', - joinCode: '邀請碼', - join: '加入', - joinMesh: '加入 Mesh', - invite: '邀請成員', - invitationTitle: '邀請成員', - invitationFor: (value: string) => `Mesh ${value}`, - invitationWarning: '此代碼只能使用一次;取得代碼的人可以讓一個 peer 加入此 Mesh。', - invitationDirectOnly: - '尚未連線至協調節點。此邀請碼只包含直接位址,跨 NAT 時可能無法連線。', - invitationExpires: (value: string) => `有效期限至 ${value}`, - invitationCopied: '邀請碼已複製', - copyInvitation: '複製邀請碼', - create: '建立 Mesh', - refresh: '重新整理', - back: '返回', - leave: '退出 Mesh', - closeMesh: '關閉 Mesh', - remove: '移除成員', - cancel: '取消', - closeConfirm: '關閉這個 Mesh?', - leaveConfirm: '退出這個 Mesh?', - removeConfirm: '移除這個成員?', - meshActions: 'Mesh 操作', - memberActions: (peerId: string) => `${peerId} 的操作`, - addLocalHost: '新增本機 Runtime Host', - localRuntimeHost: 'Runtime Host', - localHostMissing: '本機 Runtime Host 尚未加入', - localHostMissingHint: '加入後,其他成員才能透過此 Mesh 連線至本機分享的任務。', - } - : { - title: 'Peer Mesh', - experimental: 'Experimental', - failed: 'Peer Mesh operation failed', - invalidResult: 'Peer Mesh returned an invalid result', - unknownError: 'Peer Mesh operation failed', - outcomeUnknown: - 'The Host may have completed this operation. Its current state was refreshed; review it before trying again.', - invitationOutcomeUnknown: - 'The Host may have created an invitation, but its one-time code was not returned and cannot be recovered. It will expire automatically; review the pending invitation count before creating another.', - outcomeUnknownRefreshFailed: - 'The Host may have completed this operation, but its current state could not be refreshed. Reconnect and refresh before trying again.', - unavailable: 'Peer Mesh is unavailable for this endpoint', - loading: 'Loading Mesh status…', - checkingPeerConnection: "Checking this Runtime Host's peer connection…", - peerConnectionDisabled: 'Peer connectivity is not enabled for this Runtime Host', - peerConnectionDisabledHint: - 'Enable it so this Host can create or join Meshes. The existing SSH connection remains available.', - peerConnectionDisableProfileFirst: - "This Host's Direct peer connection is in use. Disable it in the Host list before changing the listener.", - enablePeerConnection: 'Enable peer connectivity', - peerConnectionStarting: 'Peer connectivity is enabled; the Mesh endpoint is starting', - peerConnectionStartingHint: 'This normally takes a few seconds. You can also check again.', - peerConnectionUpgradeRequired: 'This Runtime Host version cannot manage Peer Mesh', - peerConnectionUpgradeRequiredHint: 'Update this Host before enabling peer connectivity.', - working: { - refresh: 'Refreshing Peer Mesh…', - create: 'Creating Mesh…', - join: 'Joining Mesh…', - invite: 'Preparing invitation…', - 'add-host': 'Adding the Runtime Host to the Mesh…', - 'enable-peer': 'Enabling peer connectivity for the Runtime Host…', - update: 'Updating Mesh…', - rename: 'Saving name…', - }, - settling: 'Confirming the final state…', - endpoint: 'Manage endpoint', - desktopEndpoint: 'Desktop Client', - hostEndpoint: 'Local Runtime Host', - desktopEndpointHelp: 'This Client connects to Runtime Hosts in the Mesh.', - hostEndpointHelp: 'Add this Host so other members can reach tasks shared from this device.', - advancedSettings: 'Advanced settings', - technicalDetails: 'Identity and connectivity details', - connectivityAutomatic: 'Automatic connectivity (recommended)', - connectivityKnownRoutesOnly: 'Known routes only', - connectivityCustom: 'Custom address discovery', - restartRequired: 'Restart required', - adaptiveConnectivity: 'Adaptive connectivity', - adaptiveConnectivityHelp: - 'Maka races available direct paths automatically and uses approved member transit when needed. You do not choose a transport protocol here.', - connectivityPolicyLoading: 'Loading connectivity policy…', - connectivityPolicyLoadFailed: 'Could not load connectivity policy', - connectivityPolicySaveFailed: 'Could not save connectivity policy', - restoreDefaultConnectivityPolicy: 'Restore defaults', - connectivityPolicyRestartRequired: - 'Restart Maka to apply saved connectivity-policy changes to new connections.', - publicAddressDiscovery: 'Public address discovery', - publicStunDefault: 'Public STUN (recommended)', - publicStunDisabled: 'No public STUN', - publicStunCustom: 'Custom STUN', - customStunUrls: 'STUN addresses', - customStunUrlsInvalid: - 'Enter up to 8 comma-separated stun:host[:port] addresses.', - publicStunDefaultHelp: - 'Uses Cloudflare public STUN on a best-effort basis to discover public mappings. It never carries Maka traffic, but the provider can observe source IPs and request timing; Maka provides no availability guarantee.', - publicStunDisabledHelp: - 'Only local addresses and other known direct paths are attempted; direct connectivity across NAT may be reduced.', - publicStunCustomHelp: - 'Enter comma-separated stun: addresses. STUN discovers network addresses and never carries Session content.', - saveConnectivityPolicy: 'Save changes', - peerId: 'Peer ID', - peerIdHelp: 'Technical identity for this endpoint. Select the ID to copy its full value.', - meshId: 'Mesh ID', - meshIdHelp: 'Used to identify and diagnose this Mesh. Select the ID to copy its full value.', - thisRuntimeHost: 'This Runtime Host', - thisDesktop: 'This Desktop', - peerPathDirect: 'Direct', - peerPathTransit: 'Member transit', - peerPathOther: 'Other', - displayName: 'Name shown in the Mesh', - meshDisplayName: 'Mesh name', - unnamedMesh: 'Unnamed Mesh', - rename: 'Rename', - renameMesh: 'Rename Mesh', - save: 'Save', - peerIdCopied: 'Peer ID copied', - peerIdCopyFailed: 'Could not copy Peer ID', - meshIdCopied: 'Mesh ID copied', - copyPeerId: (value: string) => `Copy full Peer ID: ${value}`, - copyMeshId: (value: string) => `Copy full Mesh ID: ${value}`, - empty: 'Build your first Mesh', - emptyHint: 'Create a new Mesh or join one with a one-time invitation.', - meshes: 'Meshes', - mesh: 'Mesh', - members: 'Members', - activeMeshCount: (value: number) => `${value} active`, - showClosedMeshes: (value: number) => `Show closed (${value})`, - noActiveMeshes: 'No active Meshes', - noActiveMeshesHint: 'Closed Meshes are hidden by default. Use the filter above to show them.', - authority: 'Owner', - member: 'Member', - closed: 'Closed', - memberCount: (value: number) => value === 1 ? '1 member' : `${value} members`, - pending: (value: number) => `${value} pending invites`, - transit: 'Member transit', - transitHelp: 'Let members of this Mesh connect through this device using its bandwidth.', - transitToggle: 'Provide transit for this Mesh', - transitStatus: 'Member transit status', - transitLimitsLabel: 'Member transit limits', - transitLimits: (value: PeerMeshQueryResult['transit']) => - value - ? `Fixed limits: ${value.maxCircuitsPerPeer} circuits per member, ${formatHours(value.maxCircuitDurationSeconds)} per circuit, and ${formatMebibytes(value.maxCircuitBytes)}. Only one Mesh can be served at a time.` - : 'Member transit uses fixed resource limits. Only one Mesh can be served at a time.', - allowedMembers: 'Allowed members', - reservations: 'Reservations', - circuits: 'Circuits', - routeState: { - local: 'Local', - connecting: 'Connecting', - reachable: 'Reachable', - reconnecting: 'Reconnecting', - needs_repair: 'Needs a new invitation', - }, - endpointKind: { - client: 'Client', - host: 'Runtime Host', - unknown: 'Unidentified peer', - }, - endpointKindHelp: { - client: 'A Client is the interface that connects to Hosts, browses tasks, and starts actions. It does not own tasks.', - host: 'A Runtime Host owns tasks and runtime state, and executes authorized work.', - unknown: 'This peer has not reported whether it is a Client or Runtime Host, usually because it uses an older build.', - }, - joinTitle: 'Join a Mesh', - joinHint: 'Paste a one-time invitation created by another peer.', - joinCode: 'Invitation', - join: 'Join', - joinMesh: 'Join Mesh', - invite: 'Invite member', - invitationTitle: 'Invite a member', - invitationFor: (value: string) => `Mesh ${value}`, - invitationWarning: - 'This code works once. Anyone holding it can admit one peer to this Mesh.', - invitationDirectOnly: - 'No coordination peer is available yet. This invitation contains direct routes only and may not work across NATs.', - invitationExpires: (value: string) => `Expires ${value}`, - invitationCopied: 'Invitation copied', - copyInvitation: 'Copy invitation', - create: 'Create Mesh', - refresh: 'Refresh', - back: 'Back', - leave: 'Leave Mesh', - closeMesh: 'Close Mesh', - remove: 'Remove member', - cancel: 'Cancel', - closeConfirm: 'Close this Mesh?', - leaveConfirm: 'Leave this Mesh?', - removeConfirm: 'Remove this member?', - meshActions: 'Mesh actions', - memberActions: (peerId: string) => `Actions for ${peerId}`, - addLocalHost: 'Add local Runtime Host', - localRuntimeHost: 'Runtime Host', - localHostMissing: 'Local Runtime Host has not joined', - localHostMissingHint: - 'Add it so other members can reach tasks shared from this device through the Mesh.', - }; -} - -function formatHours(seconds: number): string { - return `${seconds / 3_600}h`; -} - -function formatMebibytes(bytes: number): string { - return `${bytes / (1024 * 1024)} MiB`; -} diff --git a/apps/desktop/src/renderer/locales/agent-graph-copy.ts b/apps/desktop/src/renderer/locales/agent-graph-copy.ts new file mode 100644 index 0000000000..e87370a159 --- /dev/null +++ b/apps/desktop/src/renderer/locales/agent-graph-copy.ts @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { + AgentGraphClientOperator, + AgentGraphClientSnapshot, +} from '@maka/runtime/stream-graph-read-model'; +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; + +export interface AgentGraphPanelCopy { + title: string; + loading: string; + retry: string; + collapse: string; + expand: string; + dismiss: string; + stop: string; + stopping: string; + stopFailed: string; + loadFailed: string; + openSession: string; + operators: string; + selectedResults: string; + epoch: string; + currentEpoch: string; + historicalEpoch: string; + cappedEpochs(count: number): string; + noOperators: string; + hiddenOperators(count: number): string; + progress(settled: number, total: number, hasOmitted: boolean): string; + status(status: AgentGraphClientSnapshot['status']): string; + operatorStatus(status: AgentGraphClientOperator['status']): string; + wait(operator: AgentGraphClientOperator): string | undefined; +} + +const AGENT_GRAPH_PANEL_COPY = { + 'zh-CN': { + title: 'Agent Graph', + loading: '正在读取 Graph 状态…', + retry: '重试', + collapse: '收起 Agent Graph', + expand: '展开 Agent Graph', + dismiss: '关闭 Agent Graph', + stop: '停止 Graph', + stopping: '停止中…', + stopFailed: '停止 Graph 失败,请重试。', + loadFailed: 'Graph 状态刷新失败。', + openSession: '打开子任务', + operators: 'Operators', + selectedResults: '已选择结果', + epoch: 'Graph 运行轮次', + currentEpoch: '当前', + historicalEpoch: '历史记录(只读)', + cappedEpochs: (count) => `仅显示最近 ${count} 次运行`, + noOperators: '等待主 Agent 创建 operator…', + hiddenOperators: (count) => `另有 ${count} 个 operator`, + progress: (settled, total, hasOmitted) => + hasOmitted ? `可见 ${settled}/${total} 已结束` : `${settled}/${total} 已结束`, + status: (status) => + ({ + empty: '等待调度', + active: '运行中', + closing: '收尾中', + waiting: '等待中', + stopped: '已停止', + failed: '失败', + completed: '已完成', + })[status], + operatorStatus: (status) => + ({ + not_started: '未启动', + waiting: '等待', + runnable: '可运行', + running: '运行中', + blocked: '受阻', + completed: '完成', + failed: '失败', + aborted: '中止', + cancelled: '取消', + })[status], + wait: waitReasonZh, + }, + 'zh-TW': { + title: 'Agent Graph', + loading: '正在讀取 Graph 狀態…', + retry: '重試', + collapse: '收起 Agent Graph', + expand: '展開 Agent Graph', + dismiss: '關閉 Agent Graph', + stop: '停止 Graph', + stopping: '停止中…', + stopFailed: '停止 Graph 失敗,請重試。', + loadFailed: 'Graph 狀態重新整理失敗。', + openSession: '開啟子任務', + operators: 'Operators', + selectedResults: '已選取結果', + epoch: 'Graph 執行輪次', + currentEpoch: '目前', + historicalEpoch: '歷史記錄(唯讀)', + cappedEpochs: (count) => `僅顯示最近 ${count} 次執行`, + noOperators: '等待主 Agent 建立 operator…', + hiddenOperators: (count) => `另有 ${count} 個 operator`, + progress: (settled, total, hasOmitted) => + hasOmitted ? `可見項目中 ${settled}/${total} 已結束` : `${settled}/${total} 已結束`, + status: (status) => + ({ + empty: '等待排程', + active: '執行中', + closing: '收尾中', + waiting: '等待中', + stopped: '已停止', + failed: '失敗', + completed: '已完成', + })[status], + operatorStatus: (status) => + ({ + not_started: '尚未啟動', + waiting: '等待', + runnable: '可執行', + running: '執行中', + blocked: '受阻', + completed: '完成', + failed: '失敗', + aborted: '已中止', + cancelled: '已取消', + })[status], + wait: waitReasonZhTw, + }, + en: { + title: 'Agent Graph', + loading: 'Loading graph state…', + retry: 'Retry', + collapse: 'Collapse Agent Graph', + expand: 'Expand Agent Graph', + dismiss: 'Dismiss Agent Graph', + stop: 'Stop graph', + stopping: 'Stopping…', + stopFailed: 'Could not stop the graph. Try again.', + loadFailed: 'Could not refresh graph state.', + openSession: 'Open child task', + operators: 'Operators', + selectedResults: 'Selected results', + epoch: 'Graph run', + currentEpoch: 'Current', + historicalEpoch: 'History (read-only)', + cappedEpochs: (count) => `Showing the newest ${count} runs`, + noOperators: 'Waiting for the main agent to create an operator…', + hiddenOperators: (count) => `${count} more operator${count === 1 ? '' : 's'}`, + progress: (settled, total, hasOmitted) => + hasOmitted ? `${settled}/${total} visible settled` : `${settled}/${total} settled`, + status: (status) => + ({ + empty: 'Awaiting schedule', + active: 'Running', + closing: 'Finishing', + waiting: 'Waiting', + stopped: 'Stopped', + failed: 'Failed', + completed: 'Completed', + })[status], + operatorStatus: (status) => + ({ + not_started: 'Not started', + waiting: 'Waiting', + runnable: 'Runnable', + running: 'Running', + blocked: 'Blocked', + completed: 'Completed', + failed: 'Failed', + aborted: 'Aborted', + cancelled: 'Cancelled', + })[status], + wait: waitReasonEn, + }, +} satisfies UiCatalog; + +export function getAgentGraphPanelCopy(locale: UiLocale): AgentGraphPanelCopy { + return AGENT_GRAPH_PANEL_COPY[locale]; +} + +function firstWait(operator: AgentGraphClientOperator) { + return operator.readiness.find((readiness) => readiness.status === 'waiting')?.waitingFor[0]; +} + +function waitReasonEn(operator: AgentGraphClientOperator): string | undefined { + const wait = firstWait(operator); + if (!wait) return undefined; + if (wait.kind === 'input_route') { + return `Waiting for input from ${wait.upstreamOperatorIds.join(', ')}`; + } + if (wait.kind === 'activation_missing') { + return `Waiting for ${wait.operatorId} activation`; + } + return `Waiting for ${wait.operatorId} to settle`; +} + +function waitReasonZh(operator: AgentGraphClientOperator): string | undefined { + const wait = firstWait(operator); + if (!wait) return undefined; + if (wait.kind === 'input_route') { + return `等待 ${wait.upstreamOperatorIds.join('、')} 的输入`; + } + if (wait.kind === 'activation_missing') { + return `等待 ${wait.operatorId} activation`; + } + return `等待 ${wait.operatorId} 结束`; +} + +function waitReasonZhTw(operator: AgentGraphClientOperator): string | undefined { + const wait = firstWait(operator); + if (!wait) return undefined; + if (wait.kind === 'input_route') { + return `等待 ${wait.upstreamOperatorIds.join('、')} 的輸入`; + } + if (wait.kind === 'activation_missing') { + return `等待 ${wait.operatorId} 啟用`; + } + return `等待 ${wait.operatorId} 結束`; +} diff --git a/apps/desktop/src/renderer/locales/peer-mesh-copy.ts b/apps/desktop/src/renderer/locales/peer-mesh-copy.ts new file mode 100644 index 0000000000..cd76f07b40 --- /dev/null +++ b/apps/desktop/src/renderer/locales/peer-mesh-copy.ts @@ -0,0 +1,646 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { PeerMeshQueryResult } from '@maka/runtime-host/protocol'; +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; + +export interface PeerMeshCopy { + readonly title: string; + readonly experimental: string; + readonly failed: string; + readonly invalidResult: string; + readonly unknownError: string; + readonly outcomeUnknown: string; + readonly invitationOutcomeUnknown: string; + readonly outcomeUnknownRefreshFailed: string; + readonly unavailable: string; + readonly loading: string; + readonly checkingPeerConnection: string; + readonly peerConnectionDisabled: string; + readonly peerConnectionDisabledHint: string; + readonly peerConnectionDisableProfileFirst: string; + readonly enablePeerConnection: string; + readonly peerConnectionStarting: string; + readonly peerConnectionStartingHint: string; + readonly peerConnectionUpgradeRequired: string; + readonly peerConnectionUpgradeRequiredHint: string; + readonly working: Record< + 'refresh' | 'create' | 'join' | 'invite' | 'add-host' | 'enable-peer' | 'update' | 'rename', + string + >; + readonly settling: string; + readonly endpoint: string; + readonly desktopEndpoint: string; + readonly hostEndpoint: string; + readonly desktopEndpointHelp: string; + readonly hostEndpointHelp: string; + readonly advancedSettings: string; + readonly technicalDetails: string; + readonly connectivityAutomatic: string; + readonly connectivityKnownRoutesOnly: string; + readonly connectivityCustom: string; + readonly restartRequired: string; + readonly adaptiveConnectivity: string; + readonly adaptiveConnectivityHelp: string; + readonly connectivityPolicyLoading: string; + readonly connectivityPolicyLoadFailed: string; + readonly connectivityPolicySaveFailed: string; + readonly restoreDefaultConnectivityPolicy: string; + readonly connectivityPolicyRestartRequired: string; + readonly publicAddressDiscovery: string; + readonly publicStunDefault: string; + readonly publicStunDisabled: string; + readonly publicStunCustom: string; + readonly customStunUrls: string; + readonly customStunUrlsInvalid: string; + readonly publicStunDefaultHelp: string; + readonly publicStunDisabledHelp: string; + readonly publicStunCustomHelp: string; + readonly saveConnectivityPolicy: string; + readonly peerId: string; + readonly peerIdHelp: string; + readonly meshId: string; + readonly meshIdHelp: string; + readonly thisRuntimeHost: string; + readonly thisDesktop: string; + readonly displayName: string; + readonly meshDisplayName: string; + readonly unnamedMesh: string; + readonly rename: string; + readonly renameMesh: string; + readonly save: string; + readonly peerIdCopied: string; + readonly meshIdCopied: string; + copyPeerId(value: string): string; + copyMeshId(value: string): string; + readonly empty: string; + readonly emptyHint: string; + readonly meshes: string; + readonly mesh: string; + readonly members: string; + activeMeshCount(value: number): string; + showClosedMeshes(value: number): string; + readonly noActiveMeshes: string; + readonly noActiveMeshesHint: string; + readonly authority: string; + readonly member: string; + readonly closed: string; + memberCount(value: number): string; + pending(value: number): string; + readonly transit: string; + readonly transitHelp: string; + readonly transitToggle: string; + readonly transitStatus: string; + readonly transitLimitsLabel: string; + transitLimits(value: PeerMeshQueryResult['transit']): string; + readonly allowedMembers: string; + readonly reservations: string; + readonly circuits: string; + readonly routeState: Record< + 'local' | 'connecting' | 'reachable' | 'reconnecting' | 'needs_repair' | 'unknown', + string + >; + readonly endpointKind: Record<'client' | 'host' | 'unknown', string>; + readonly endpointKindHelp: Record<'client' | 'host' | 'unknown', string>; + readonly joinTitle: string; + readonly joinHint: string; + readonly joinCode: string; + readonly join: string; + readonly joinMesh: string; + readonly invite: string; + readonly invitationTitle: string; + invitationFor(value: string): string; + readonly invitationWarning: string; + readonly invitationDirectOnly: string; + invitationExpires(value: string): string; + readonly invitationCopied: string; + readonly copyInvitation: string; + readonly create: string; + readonly refresh: string; + readonly back: string; + readonly leave: string; + readonly closeMesh: string; + readonly remove: string; + readonly cancel: string; + readonly closeConfirm: string; + readonly leaveConfirm: string; + readonly removeConfirm: string; + readonly meshActions: string; + memberActions(peerId: string): string; + readonly addLocalHost: string; + readonly localRuntimeHost: string; + readonly localHostMissing: string; + readonly localHostMissingHint: string; +} + +const PEER_MESH_COPY = { + 'zh-CN': { + title: 'Peer Mesh', + experimental: '实验性', + failed: 'Peer Mesh 操作失败', + invalidResult: 'Peer Mesh 返回了无效结果', + unknownError: 'Peer Mesh 操作失败', + outcomeUnknown: 'Host 可能已完成此操作。已刷新当前状态,请确认后再重试。', + invitationOutcomeUnknown: + 'Host 可能已创建邀请码,但代码未能返回且无法恢复。它会自动过期;创建新邀请前请先检查待使用邀请数量。', + outcomeUnknownRefreshFailed: + 'Host 可能已完成此操作,但当前状态也未能刷新。请恢复连接并刷新后再重试。', + unavailable: '当前 endpoint 不支持 Peer Mesh', + loading: '正在读取 Mesh 状态…', + checkingPeerConnection: '正在检查此 Runtime Host 的 Peer 连接…', + peerConnectionDisabled: '此 Runtime Host 尚未开启 Peer 连接', + peerConnectionDisabledHint: + '开启后,此 Host 才能创建或加入 Mesh;现有 SSH 连接会继续保留。', + peerConnectionDisableProfileFirst: + '此 Host 的 Direct peer 连接正在使用中,请先在 Host 列表中将它停用。', + enablePeerConnection: '开启 Peer 连接', + peerConnectionStarting: 'Peer 连接已开启,Mesh endpoint 正在就绪', + peerConnectionStartingHint: '通常只需几秒;也可以立即重新检查。', + peerConnectionUpgradeRequired: '此 Runtime Host 版本尚不支持 Peer Mesh 管理', + peerConnectionUpgradeRequiredHint: '请先更新此 Host,再回来开启 Peer 连接。', + working: { + refresh: '正在刷新 Peer Mesh…', + create: '正在创建 Mesh…', + join: '正在加入 Mesh…', + invite: '正在准备邀请…', + 'add-host': '正在将 Runtime Host 加入 Mesh…', + 'enable-peer': '正在为 Runtime Host 开启 Peer 连接…', + update: '正在更新 Mesh…', + rename: '正在保存名称…', + }, + settling: '正在确认最终状态…', + endpoint: '管理对象', + desktopEndpoint: 'Desktop Client', + hostEndpoint: '本机 Runtime Host', + desktopEndpointHelp: '此 Client 用于连接 Mesh 中的 Runtime Host。', + hostEndpointHelp: '此 Host 加入后,其他成员才能连接本机分享的任务。', + advancedSettings: '高级设置', + technicalDetails: '身份与连接配置', + connectivityAutomatic: '自动连接(推荐)', + connectivityKnownRoutesOnly: '仅使用已知路径', + connectivityCustom: '自定义地址发现', + restartRequired: '需要重启', + adaptiveConnectivity: '自适应连接', + adaptiveConnectivityHelp: + 'Maka 会自动竞速可用的直连方式,并在获准时使用成员转发;这里不需要选择具体协议。', + connectivityPolicyLoading: '正在读取连接策略…', + connectivityPolicyLoadFailed: '无法读取连接策略', + connectivityPolicySaveFailed: '无法保存连接策略', + restoreDefaultConnectivityPolicy: '恢复默认设置', + connectivityPolicyRestartRequired: + '重启 Maka 后,已保存的连接策略变更会应用到新连接。', + publicAddressDiscovery: '公网地址发现', + publicStunDefault: '公共 STUN(推荐)', + publicStunDisabled: '不使用公共 STUN', + publicStunCustom: '自定义 STUN', + customStunUrls: 'STUN 地址', + customStunUrlsInvalid: + '请输入以逗号分隔的 stun:主机[:端口] 地址,最多 8 个。', + publicStunDefaultHelp: + '使用 Cloudflare 公共 STUN 尽力发现公网映射。它不承载 Maka 流量,但提供方可观察源 IP 和请求时间;Maka 不保证其可用性。', + publicStunDisabledHelp: + '仅尝试本地地址和其他已知直连路径;跨 NAT 的直连成功率可能降低。', + publicStunCustomHelp: + '使用逗号分隔的 stun: 地址。STUN 只发现网络地址,不承载 Session 内容。', + saveConnectivityPolicy: '保存更改', + peerId: 'Peer ID', + peerIdHelp: '此 endpoint 在 Mesh 中的技术身份;点击 ID 可复制完整值。', + meshId: 'Mesh ID', + meshIdHelp: '用于诊断和识别此 Mesh;点击 ID 可复制完整值。', + thisRuntimeHost: '本机 Runtime Host', + thisDesktop: '本机 Desktop', + displayName: '在 Mesh 中显示的名称', + meshDisplayName: 'Mesh 名称', + unnamedMesh: '未命名 Mesh', + rename: '修改名称', + renameMesh: '修改 Mesh 名称', + save: '保存', + peerIdCopied: 'Peer ID 已复制', + meshIdCopied: 'Mesh ID 已复制', + copyPeerId: (value: string) => `复制完整 Peer ID:${value}`, + copyMeshId: (value: string) => `复制完整 Mesh ID:${value}`, + empty: '建立你的第一个 Mesh', + emptyHint: '创建新 Mesh,或通过一次性邀请码加入。', + meshes: 'Mesh', + mesh: 'Mesh', + members: '成员', + activeMeshCount: (value: number) => `${value} 个使用中`, + showClosedMeshes: (value: number) => `显示已关闭(${value})`, + noActiveMeshes: '没有使用中的 Mesh', + noActiveMeshesHint: '已关闭的 Mesh 默认隐藏;可通过上方筛选查看。', + authority: '管理者', + member: '成员', + closed: '已关闭', + memberCount: (value: number) => `${value} 个成员`, + pending: (value: number) => `${value} 个待使用邀请`, + transit: '成员转发', + transitHelp: '允许此 Mesh 的成员通过本机建立连接;会使用本机带宽。', + transitToggle: '为此 Mesh 提供转发', + transitStatus: '成员转发状态', + transitLimitsLabel: '成员转发限制', + transitLimits: (value: PeerMeshQueryResult['transit']) => + value + ? `固定上限:每个成员 ${value.maxCircuitsPerPeer} 条连接,每条最长 ${formatHours(value.maxCircuitDurationSeconds)},最多 ${formatMebibytes(value.maxCircuitBytes)}。一次只能为一个 Mesh 开启。` + : '成员转发使用固定资源上限,一次只能为一个 Mesh 开启。', + allowedMembers: '允许成员', + reservations: 'Reservation', + circuits: '连接', + routeState: { + local: '本机', + connecting: '正在连接', + reachable: '可达', + reconnecting: '正在恢复连接', + needs_repair: '需要新邀请码修复', + unknown: '路径未知', + }, + endpointKind: { + client: 'Client', + host: 'Runtime Host', + unknown: '未标识 Peer', + }, + endpointKindHelp: { + client: 'Client 是操作界面:它连接 Host、浏览任务并发起操作,本身不持有任务。', + host: 'Runtime Host 持有任务和运行状态,并执行经过授权的工作。', + unknown: '此 Peer 尚未报告它是 Client 还是 Runtime Host,通常来自旧版本。', + }, + joinTitle: '加入 Mesh', + joinHint: '粘贴另一个 Peer 生成的一次性邀请码。', + joinCode: '邀请码', + join: '加入', + joinMesh: '加入 Mesh', + invite: '邀请成员', + invitationTitle: '邀请成员', + invitationFor: (value: string) => `Mesh ${value}`, + invitationWarning: '该代码只能使用一次;获得代码的人可以让一个 peer 加入此 Mesh。', + invitationDirectOnly: + '尚未连接到协调节点。此邀请码只包含直接地址,跨 NAT 时可能无法连接。', + invitationExpires: (value: string) => `有效期至 ${value}`, + invitationCopied: '邀请码已复制', + copyInvitation: '复制邀请码', + create: '创建 Mesh', + refresh: '刷新', + back: '返回', + leave: '退出 Mesh', + closeMesh: '关闭 Mesh', + remove: '移除成员', + cancel: '取消', + closeConfirm: '关闭这个 Mesh?', + leaveConfirm: '退出这个 Mesh?', + removeConfirm: '移除这个成员?', + meshActions: 'Mesh 操作', + memberActions: (peerId: string) => `${peerId} 的操作`, + addLocalHost: '添加本机 Runtime Host', + localRuntimeHost: 'Runtime Host', + localHostMissing: '本机 Runtime Host 尚未加入', + localHostMissingHint: '加入后,其他成员才能通过此 Mesh 连接本机分享的任务。', + }, + 'zh-TW': { + title: 'Peer Mesh', + experimental: '實驗性', + failed: 'Peer Mesh 操作失敗', + invalidResult: 'Peer Mesh 回傳了無效結果', + unknownError: 'Peer Mesh 操作失敗', + outcomeUnknown: 'Host 可能已完成此操作。已重新整理目前狀態,請確認後再重試。', + invitationOutcomeUnknown: + 'Host 可能已建立邀請碼,但代碼未能回傳且無法復原。它會自動過期;建立新邀請前請先檢查待使用邀請數量。', + outcomeUnknownRefreshFailed: + 'Host 可能已完成此操作,但目前狀態也無法重新整理。請恢復連線並重新整理後再重試。', + unavailable: '目前 endpoint 不支援 Peer Mesh', + loading: '正在讀取 Mesh 狀態…', + checkingPeerConnection: '正在檢查此 Runtime Host 的 Peer 連線…', + peerConnectionDisabled: '此 Runtime Host 尚未開啟 Peer 連線', + peerConnectionDisabledHint: + '開啟後,此 Host 才能建立或加入 Mesh;現有 SSH 連線會繼續保留。', + peerConnectionDisableProfileFirst: + '此 Host 的 Direct peer 連線正在使用中,請先在 Host 列表中將它停用。', + enablePeerConnection: '開啟 Peer 連線', + peerConnectionStarting: 'Peer 連線已開啟,Mesh endpoint 正在就緒', + peerConnectionStartingHint: '通常只需幾秒;也可以立即重新檢查。', + peerConnectionUpgradeRequired: '此 Runtime Host 版本尚不支援 Peer Mesh 管理', + peerConnectionUpgradeRequiredHint: '請先更新此 Host,再回來開啟 Peer 連線。', + working: { + refresh: '正在重新整理 Peer Mesh…', + create: '正在建立 Mesh…', + join: '正在加入 Mesh…', + invite: '正在準備邀請…', + 'add-host': '正在將 Runtime Host 加入 Mesh…', + 'enable-peer': '正在為 Runtime Host 開啟 Peer 連線…', + update: '正在更新 Mesh…', + rename: '正在儲存名稱…', + }, + settling: '正在確認最終狀態…', + endpoint: '管理對象', + desktopEndpoint: 'Desktop Client', + hostEndpoint: '本機 Runtime Host', + desktopEndpointHelp: '此 Client 用於連線至 Mesh 中的 Runtime Host。', + hostEndpointHelp: '此 Host 加入後,其他成員才能連線至本機分享的任務。', + advancedSettings: '進階設定', + technicalDetails: '身分與連線設定', + connectivityAutomatic: '自動連線(建議)', + connectivityKnownRoutesOnly: '僅使用已知路徑', + connectivityCustom: '自訂位址探索', + restartRequired: '需要重新啟動', + adaptiveConnectivity: '自適應連線', + adaptiveConnectivityHelp: + 'Maka 會自動競速可用的直接連線方式,並在獲准時使用成員轉送;此處不需要選擇特定通訊協定。', + connectivityPolicyLoading: '正在讀取連線策略…', + connectivityPolicyLoadFailed: '無法讀取連線策略', + connectivityPolicySaveFailed: '無法儲存連線策略', + restoreDefaultConnectivityPolicy: '恢復預設設定', + connectivityPolicyRestartRequired: + '重新啟動 Maka 後,已儲存的連線策略變更會套用至新連線。', + publicAddressDiscovery: '公網位址探索', + publicStunDefault: '公共 STUN(建議)', + publicStunDisabled: '不使用公共 STUN', + publicStunCustom: '自訂 STUN', + customStunUrls: 'STUN 位址', + customStunUrlsInvalid: '請輸入以逗號分隔的 stun:主機[:連接埠] 位址,最多 8 個。', + publicStunDefaultHelp: + '使用 Cloudflare 公共 STUN 盡力探索公網對映。它不承載 Maka 流量,但提供者可觀察來源 IP 和請求時間;Maka 不保證其可用性。', + publicStunDisabledHelp: + '僅嘗試本機位址和其他已知直接連線路徑;跨 NAT 的直接連線成功率可能降低。', + publicStunCustomHelp: + '使用逗號分隔的 stun: 位址。STUN 只探索網路位址,不承載 Session 內容。', + saveConnectivityPolicy: '儲存變更', + peerId: 'Peer ID', + peerIdHelp: '此 endpoint 在 Mesh 中的技術身分;點選 ID 可複製完整值。', + meshId: 'Mesh ID', + meshIdHelp: '用於診斷和識別此 Mesh;點選 ID 可複製完整值。', + thisRuntimeHost: '本機 Runtime Host', + thisDesktop: '本機 Desktop', + displayName: '在 Mesh 中顯示的名稱', + meshDisplayName: 'Mesh 名稱', + unnamedMesh: '未命名 Mesh', + rename: '修改名稱', + renameMesh: '修改 Mesh 名稱', + save: '儲存', + peerIdCopied: 'Peer ID 已複製', + meshIdCopied: 'Mesh ID 已複製', + copyPeerId: (value: string) => `複製完整 Peer ID:${value}`, + copyMeshId: (value: string) => `複製完整 Mesh ID:${value}`, + empty: '建立你的第一個 Mesh', + emptyHint: '建立新的 Mesh,或透過一次性邀請碼加入。', + meshes: 'Mesh', + mesh: 'Mesh', + members: '成員', + activeMeshCount: (value: number) => `${value} 個使用中`, + showClosedMeshes: (value: number) => `顯示已關閉(${value})`, + noActiveMeshes: '沒有使用中的 Mesh', + noActiveMeshesHint: '已關閉的 Mesh 預設隱藏;可透過上方篩選查看。', + authority: '管理者', + member: '成員', + closed: '已關閉', + memberCount: (value: number) => `${value} 個成員`, + pending: (value: number) => `${value} 個待使用邀請`, + transit: '成員轉送', + transitHelp: '允許此 Mesh 的成員透過本機建立連線;會使用本機頻寬。', + transitToggle: '為此 Mesh 提供轉送', + transitStatus: '成員轉送狀態', + transitLimitsLabel: '成員轉送限制', + transitLimits: (value: PeerMeshQueryResult['transit']) => + value + ? `固定上限:每位成員 ${value.maxCircuitsPerPeer} 條連線,每條最長 ${formatHours(value.maxCircuitDurationSeconds)},最多 ${formatMebibytes(value.maxCircuitBytes)}。一次只能為一個 Mesh 開啟。` + : '成員轉送使用固定資源上限,一次只能為一個 Mesh 開啟。', + allowedMembers: '允許成員', + reservations: 'Reservation', + circuits: '連線', + routeState: { + local: '本機', + connecting: '正在連線', + reachable: '可連線', + reconnecting: '正在恢復連線', + needs_repair: '需要新邀請碼修復', + }, + endpointKind: { + client: 'Client', + host: 'Runtime Host', + unknown: '未識別 Peer', + }, + endpointKindHelp: { + client: 'Client 是操作介面:它連線至 Host、瀏覽任務並發起操作,本身不持有任務。', + host: 'Runtime Host 持有任務和執行狀態,並執行經過授權的工作。', + unknown: '此 Peer 尚未回報它是 Client 或 Runtime Host,通常來自舊版本。', + }, + joinTitle: '加入 Mesh', + joinHint: '貼上另一個 Peer 產生的一次性邀請碼。', + joinCode: '邀請碼', + join: '加入', + joinMesh: '加入 Mesh', + invite: '邀請成員', + invitationTitle: '邀請成員', + invitationFor: (value: string) => `Mesh ${value}`, + invitationWarning: '此代碼只能使用一次;取得代碼的人可以讓一個 peer 加入此 Mesh。', + invitationDirectOnly: + '尚未連線至協調節點。此邀請碼只包含直接位址,跨 NAT 時可能無法連線。', + invitationExpires: (value: string) => `有效期限至 ${value}`, + invitationCopied: '邀請碼已複製', + copyInvitation: '複製邀請碼', + create: '建立 Mesh', + refresh: '重新整理', + back: '返回', + leave: '退出 Mesh', + closeMesh: '關閉 Mesh', + remove: '移除成員', + cancel: '取消', + closeConfirm: '關閉這個 Mesh?', + leaveConfirm: '退出這個 Mesh?', + removeConfirm: '移除這個成員?', + meshActions: 'Mesh 操作', + memberActions: (peerId: string) => `${peerId} 的操作`, + addLocalHost: '新增本機 Runtime Host', + localRuntimeHost: 'Runtime Host', + localHostMissing: '本機 Runtime Host 尚未加入', + localHostMissingHint: '加入後,其他成員才能透過此 Mesh 連線至本機分享的任務。', + }, + en: { + title: 'Peer Mesh', + experimental: 'Experimental', + failed: 'Peer Mesh operation failed', + invalidResult: 'Peer Mesh returned an invalid result', + unknownError: 'Peer Mesh operation failed', + outcomeUnknown: + 'The Host may have completed this operation. Its current state was refreshed; review it before trying again.', + invitationOutcomeUnknown: + 'The Host may have created an invitation, but its one-time code was not returned and cannot be recovered. It will expire automatically; review the pending invitation count before creating another.', + outcomeUnknownRefreshFailed: + 'The Host may have completed this operation, but its current state could not be refreshed. Reconnect and refresh before trying again.', + unavailable: 'Peer Mesh is unavailable for this endpoint', + loading: 'Loading Mesh status…', + checkingPeerConnection: "Checking this Runtime Host's peer connection…", + peerConnectionDisabled: 'Peer connectivity is not enabled for this Runtime Host', + peerConnectionDisabledHint: + 'Enable it so this Host can create or join Meshes. The existing SSH connection remains available.', + peerConnectionDisableProfileFirst: + "This Host's Direct peer connection is in use. Disable it in the Host list before changing the listener.", + enablePeerConnection: 'Enable peer connectivity', + peerConnectionStarting: 'Peer connectivity is enabled; the Mesh endpoint is starting', + peerConnectionStartingHint: 'This normally takes a few seconds. You can also check again.', + peerConnectionUpgradeRequired: 'This Runtime Host version cannot manage Peer Mesh', + peerConnectionUpgradeRequiredHint: 'Update this Host before enabling peer connectivity.', + working: { + refresh: 'Refreshing Peer Mesh…', + create: 'Creating Mesh…', + join: 'Joining Mesh…', + invite: 'Preparing invitation…', + 'add-host': 'Adding the Runtime Host to the Mesh…', + 'enable-peer': 'Enabling peer connectivity for the Runtime Host…', + update: 'Updating Mesh…', + rename: 'Saving name…', + }, + settling: 'Confirming the final state…', + endpoint: 'Manage endpoint', + desktopEndpoint: 'Desktop Client', + hostEndpoint: 'Local Runtime Host', + desktopEndpointHelp: 'This Client connects to Runtime Hosts in the Mesh.', + hostEndpointHelp: 'Add this Host so other members can reach tasks shared from this device.', + advancedSettings: 'Advanced settings', + technicalDetails: 'Identity and connectivity details', + connectivityAutomatic: 'Automatic connectivity (recommended)', + connectivityKnownRoutesOnly: 'Known routes only', + connectivityCustom: 'Custom address discovery', + restartRequired: 'Restart required', + adaptiveConnectivity: 'Adaptive connectivity', + adaptiveConnectivityHelp: + 'Maka races available direct paths automatically and uses approved member transit when needed. You do not choose a transport protocol here.', + connectivityPolicyLoading: 'Loading connectivity policy…', + connectivityPolicyLoadFailed: 'Could not load connectivity policy', + connectivityPolicySaveFailed: 'Could not save connectivity policy', + restoreDefaultConnectivityPolicy: 'Restore defaults', + connectivityPolicyRestartRequired: + 'Restart Maka to apply saved connectivity-policy changes to new connections.', + publicAddressDiscovery: 'Public address discovery', + publicStunDefault: 'Public STUN (recommended)', + publicStunDisabled: 'No public STUN', + publicStunCustom: 'Custom STUN', + customStunUrls: 'STUN addresses', + customStunUrlsInvalid: + 'Enter up to 8 comma-separated stun:host[:port] addresses.', + publicStunDefaultHelp: + 'Uses Cloudflare public STUN on a best-effort basis to discover public mappings. It never carries Maka traffic, but the provider can observe source IPs and request timing; Maka provides no availability guarantee.', + publicStunDisabledHelp: + 'Only local addresses and other known direct paths are attempted; direct connectivity across NAT may be reduced.', + publicStunCustomHelp: + 'Enter comma-separated stun: addresses. STUN discovers network addresses and never carries Session content.', + saveConnectivityPolicy: 'Save changes', + peerId: 'Peer ID', + peerIdHelp: 'Technical identity for this endpoint. Select the ID to copy its full value.', + meshId: 'Mesh ID', + meshIdHelp: 'Used to identify and diagnose this Mesh. Select the ID to copy its full value.', + thisRuntimeHost: 'This Runtime Host', + thisDesktop: 'This Desktop', + displayName: 'Name shown in the Mesh', + meshDisplayName: 'Mesh name', + unnamedMesh: 'Unnamed Mesh', + rename: 'Rename', + renameMesh: 'Rename Mesh', + save: 'Save', + peerIdCopied: 'Peer ID copied', + meshIdCopied: 'Mesh ID copied', + copyPeerId: (value: string) => `Copy full Peer ID: ${value}`, + copyMeshId: (value: string) => `Copy full Mesh ID: ${value}`, + empty: 'Build your first Mesh', + emptyHint: 'Create a new Mesh or join one with a one-time invitation.', + meshes: 'Meshes', + mesh: 'Mesh', + members: 'Members', + activeMeshCount: (value: number) => `${value} active`, + showClosedMeshes: (value: number) => `Show closed (${value})`, + noActiveMeshes: 'No active Meshes', + noActiveMeshesHint: 'Closed Meshes are hidden by default. Use the filter above to show them.', + authority: 'Owner', + member: 'Member', + closed: 'Closed', + memberCount: (value: number) => value === 1 ? '1 member' : `${value} members`, + pending: (value: number) => `${value} pending invites`, + transit: 'Member transit', + transitHelp: 'Let members of this Mesh connect through this device using its bandwidth.', + transitToggle: 'Provide transit for this Mesh', + transitStatus: 'Member transit status', + transitLimitsLabel: 'Member transit limits', + transitLimits: (value: PeerMeshQueryResult['transit']) => + value + ? `Fixed limits: ${value.maxCircuitsPerPeer} circuits per member, ${formatHours(value.maxCircuitDurationSeconds)} per circuit, and ${formatMebibytes(value.maxCircuitBytes)}. Only one Mesh can be served at a time.` + : 'Member transit uses fixed resource limits. Only one Mesh can be served at a time.', + allowedMembers: 'Allowed members', + reservations: 'Reservations', + circuits: 'Circuits', + routeState: { + local: 'Local', + connecting: 'Connecting', + reachable: 'Reachable', + reconnecting: 'Reconnecting', + needs_repair: 'Needs a new invitation', + unknown: 'Route unknown', + }, + endpointKind: { + client: 'Client', + host: 'Runtime Host', + unknown: 'Unidentified peer', + }, + endpointKindHelp: { + client: 'A Client is the interface that connects to Hosts, browses tasks, and starts actions. It does not own tasks.', + host: 'A Runtime Host owns tasks and runtime state, and executes authorized work.', + unknown: 'This peer has not reported whether it is a Client or Runtime Host, usually because it uses an older build.', + }, + joinTitle: 'Join a Mesh', + joinHint: 'Paste a one-time invitation created by another peer.', + joinCode: 'Invitation', + join: 'Join', + joinMesh: 'Join Mesh', + invite: 'Invite member', + invitationTitle: 'Invite a member', + invitationFor: (value: string) => `Mesh ${value}`, + invitationWarning: + 'This code works once. Anyone holding it can admit one peer to this Mesh.', + invitationDirectOnly: + 'No coordination peer is available yet. This invitation contains direct routes only and may not work across NATs.', + invitationExpires: (value: string) => `Expires ${value}`, + invitationCopied: 'Invitation copied', + copyInvitation: 'Copy invitation', + create: 'Create Mesh', + refresh: 'Refresh', + back: 'Back', + leave: 'Leave Mesh', + closeMesh: 'Close Mesh', + remove: 'Remove member', + cancel: 'Cancel', + closeConfirm: 'Close this Mesh?', + leaveConfirm: 'Leave this Mesh?', + removeConfirm: 'Remove this member?', + meshActions: 'Mesh actions', + memberActions: (peerId: string) => `Actions for ${peerId}`, + addLocalHost: 'Add local Runtime Host', + localRuntimeHost: 'Runtime Host', + localHostMissing: 'Local Runtime Host has not joined', + localHostMissingHint: + 'Add it so other members can reach tasks shared from this device through the Mesh.', + }, +} satisfies UiCatalog; + +export function getPeerMeshCopy(locale: UiLocale): PeerMeshCopy { + return PEER_MESH_COPY[locale]; +} + +function formatHours(seconds: number): string { + return `${seconds / 3_600}h`; +} + +function formatMebibytes(bytes: number): string { + return `${bytes / (1024 * 1024)} MiB`; +} diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts index c393ee1f08..4ceea76a8b 100644 --- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts @@ -121,6 +121,12 @@ export type SettingsProjectsCopy = { saveAndEnable: string; defaultBadge: string; experimentalBadge: string; + copyPeerId(value: string): string; + peerIdCopied: string; + peerIdCopyFailed: string; + peerPathDirect: string; + peerPathTransit: string; + peerPathTransportOther: string; defaultDisableHelp: string; unavailable: string; manage: string; @@ -423,6 +429,12 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { saveAndEnable: '保存并启用', defaultBadge: '默认', experimentalBadge: '实验性', + copyPeerId: (value: string) => `复制完整 Peer ID:${value}`, + peerIdCopied: 'Peer ID 已复制', + peerIdCopyFailed: '无法复制 Peer ID', + peerPathDirect: '直连', + peerPathTransit: '成员转发', + peerPathTransportOther: '其他', defaultDisableHelp: '先选择另一个默认 Host,才能停用此 Host', unavailable: '无法连接', manage: '管理', @@ -732,6 +744,12 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { credentialHelp: '在遠端機器使用 desktop-client preset 簽發', saveAndEnable: '儲存並啟用', defaultBadge: '預設', + copyPeerId: (value: string) => `複製完整 Peer ID:${value}`, + peerIdCopied: 'Peer ID 已複製', + peerIdCopyFailed: '無法複製 Peer ID', + peerPathDirect: '直接連線', + peerPathTransit: '成員轉送', + peerPathTransportOther: '其他', defaultDisableHelp: '先選擇另一個預設 Host,才能停用此 Host', unavailable: '無法連線', manage: '管理', @@ -1043,6 +1061,12 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { saveAndEnable: 'Save and enable', defaultBadge: 'Default', experimentalBadge: 'Experimental', + copyPeerId: (value: string) => `Copy full Peer ID: ${value}`, + peerIdCopied: 'Peer ID copied', + peerIdCopyFailed: 'Could not copy Peer ID', + peerPathDirect: 'Direct', + peerPathTransit: 'Member transit', + peerPathTransportOther: 'Other', defaultDisableHelp: 'Choose another default Host before disabling this Host', unavailable: 'Unavailable', manage: 'Manage', diff --git a/apps/desktop/src/renderer/locales/task-readiness-copy.ts b/apps/desktop/src/renderer/locales/task-readiness-copy.ts new file mode 100644 index 0000000000..0c66a86515 --- /dev/null +++ b/apps/desktop/src/renderer/locales/task-readiness-copy.ts @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; + +export interface TaskReadinessCopy { + readonly runtime: { + readonly title: string; + readonly description: string; + readonly actionLabel: string; + }; + readonly workspace: { + readonly title: string; + readonly description: string; + readonly actionLabel: Record<'workspace_picker' | 'retry', string>; + }; +} + +const TASK_READINESS_COPY = { + 'zh-CN': { + runtime: { + title: 'Maka 运行服务暂时不可用。', + description: '任务尚未提交。重新检测运行服务后再试。', + actionLabel: '重新检测', + }, + workspace: { + title: '当前任务的工作区不可用。', + description: '原目录可能已移动、删除或无法访问。请选择可用工作区。', + actionLabel: { workspace_picker: '选择工作区', retry: '重新检测' }, + }, + }, + 'zh-TW': { + runtime: { + title: 'Maka 執行服務暫時無法使用。', + description: '任務尚未送出。重新檢查執行服務後再試。', + actionLabel: '重新檢查', + }, + workspace: { + title: '目前任務的工作區無法使用。', + description: '原始資料夾可能已移動、刪除或無法存取。請選擇可用的工作區。', + actionLabel: { workspace_picker: '選擇工作區', retry: '重新檢查' }, + }, + }, + en: { + runtime: { + title: 'The Maka runtime is unavailable.', + description: 'The task was not submitted. Check the runtime again before retrying.', + actionLabel: 'Check again', + }, + workspace: { + title: 'This task workspace is unavailable.', + description: 'The folder may have moved, been deleted, or become inaccessible. Choose an available workspace.', + actionLabel: { workspace_picker: 'Choose workspace', retry: 'Check again' }, + }, + }, +} satisfies UiCatalog; + +export function getTaskReadinessCopy(locale: UiLocale): TaskReadinessCopy { + return TASK_READINESS_COPY[locale]; +} diff --git a/apps/desktop/src/renderer/mcp-catalog.ts b/apps/desktop/src/renderer/mcp-catalog.ts index 9d5b834ecf..108d645487 100644 --- a/apps/desktop/src/renderer/mcp-catalog.ts +++ b/apps/desktop/src/renderer/mcp-catalog.ts @@ -17,7 +17,7 @@ * under the License. */ -import type { UiLocale } from '@maka/core/ui-locale'; +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; import type { McpServerConfig } from '@maka/core/mcp'; export type McpCatalogId = @@ -223,43 +223,50 @@ type McpCatalogLocalizedCopy = Pick = { - dingtalk: { name: 'DingTalk', description: 'Manage contacts, calendars, tasks, and collaboration data.', category: 'Communication', setupLabel: 'Requires Client ID and Client Secret' }, - feishu: { name: 'Feishu', description: 'Access Feishu documents, calendars, messages, and OpenAPI.', category: 'Communication', setupLabel: 'Requires App ID and App Secret' }, - slack: { name: 'Slack', description: 'Send messages, manage channels, and collaborate in a Slack workspace.', category: 'Communication', setupLabel: 'Requires Bot Token and Team ID' }, - line: { name: 'LINE', description: 'Send and manage messages through the LINE Bot Messaging API.', category: 'Communication', setupLabel: 'Requires Channel Access Token' }, - notion: { name: 'Notion', description: 'Search, read, and update a Notion workspace.', category: 'Knowledge and documents', setupLabel: 'Requires sign-in authorization' }, - 'macos-apps': { name: 'macOS apps', description: 'Connect Calendar and Reminders through native system permissions.', category: 'System and productivity', setupLabel: undefined }, - 'google-calendar': { name: 'Google Calendar', description: 'Manage events, create meetings, and check availability.', category: 'System and productivity', setupLabel: 'Requires an OAuth credentials file' }, - figma: { name: 'Figma', description: 'Read design files, components, and developer handoff data.', category: 'Design and development', setupLabel: 'Requires a Personal Access Token' }, - vercel: { name: 'Vercel', description: 'Inspect projects, deployments, logs, and platform documentation.', category: 'Design and development', setupLabel: 'Requires sign-in authorization' }, - supabase: { name: 'Supabase', description: 'Manage databases, project configuration, migrations, and Edge Functions.', category: 'Design and development', setupLabel: 'Requires sign-in authorization' }, - filesystem: { name: 'Local files', description: 'Safely read, write, and manage files in selected directories.', category: 'Files and knowledge', setupLabel: 'Requires selecting allowed directories' }, - memory: { name: 'Persistent memory', description: 'Remember entities, relationships, and important facts in a structured knowledge graph.', category: 'Files and knowledge', setupLabel: undefined }, - playwright: { name: 'Browser automation', description: 'Let Maka read and operate real web pages through Playwright.', category: 'Design and development', setupLabel: undefined }, - 'sequential-thinking': { name: 'Sequential thinking', description: 'Provide revisable, verifiable structured reasoning for complex problems.', category: 'Reasoning and planning', setupLabel: undefined }, -}; - -const MCP_CATALOG_TRADITIONAL_COPY: Record = { - dingtalk: { name: 'DingTalk', description: '管理聯絡人、行事曆、任務和協作資料。', category: '通訊', setupLabel: '需要 Client ID 和 Client Secret' }, - feishu: { name: '飛書', description: '存取飛書文件、行事曆、訊息和 OpenAPI。', category: '通訊', setupLabel: '需要 App ID 和 App Secret' }, - slack: { name: 'Slack', description: '傳送訊息、管理頻道,並在 Slack 工作區協作。', category: '通訊', setupLabel: '需要 Bot Token 和 Team ID' }, - line: { name: 'LINE', description: '透過 LINE Bot Messaging API 傳送和管理訊息。', category: '通訊', setupLabel: '需要 Channel Access Token' }, - notion: { name: 'Notion', description: '搜尋、讀取和更新 Notion 工作區。', category: '知識與文件', setupLabel: '需要登入授權' }, - 'macos-apps': { name: 'macOS 應用程式', description: '透過原生系統權限連接「行事曆」與「提醒事項」。', category: '系統與生產力', setupLabel: undefined }, - 'google-calendar': { name: 'Google 日曆', description: '管理活動、建立會議並檢查可用時間。', category: '系統與生產力', setupLabel: '需要 OAuth 憑證檔案' }, - figma: { name: 'Figma', description: '讀取設計檔案、元件和開發交付資料。', category: '設計與開發', setupLabel: '需要 Personal Access Token' }, - vercel: { name: 'Vercel', description: '檢查專案、部署、記錄和平台文件。', category: '設計與開發', setupLabel: '需要登入授權' }, - supabase: { name: 'Supabase', description: '管理資料庫、專案設定、移轉和 Edge Functions。', category: '設計與開發', setupLabel: '需要登入授權' }, - filesystem: { name: '本機檔案', description: '在選取的資料夾中安全地讀取、寫入和管理檔案。', category: '檔案與知識', setupLabel: '需要選擇允許存取的資料夾' }, - memory: { name: '持久記憶', description: '使用結構化知識圖譜記住實體、關係和重要事實。', category: '檔案與知識', setupLabel: undefined }, - playwright: { name: '瀏覽器自動化', description: '讓 Maka 透過 Playwright 讀取和操作實際網頁。', category: '設計與開發', setupLabel: undefined }, - 'sequential-thinking': { name: '循序思考', description: '為複雜問題提供可修正、可驗證的結構化推理。', category: '推理與規劃', setupLabel: undefined }, -}; +// zh copy is authored on the base entries; both locales resolve through this one catalog. +const MCP_CATALOG_COPY = { + 'zh-CN': Object.fromEntries( + MCP_CATALOG.map((entry) => [ + entry.id, + { name: entry.name, description: entry.description, category: entry.category, setupLabel: entry.setupLabel }, + ]), + ) as Record, + 'zh-TW': { + dingtalk: { name: 'DingTalk', description: '管理聯絡人、行事曆、任務和協作資料。', category: '通訊', setupLabel: '需要 Client ID 和 Client Secret' }, + feishu: { name: '飛書', description: '存取飛書文件、行事曆、訊息和 OpenAPI。', category: '通訊', setupLabel: '需要 App ID 和 App Secret' }, + slack: { name: 'Slack', description: '傳送訊息、管理頻道,並在 Slack 工作區協作。', category: '通訊', setupLabel: '需要 Bot Token 和 Team ID' }, + line: { name: 'LINE', description: '透過 LINE Bot Messaging API 傳送和管理訊息。', category: '通訊', setupLabel: '需要 Channel Access Token' }, + notion: { name: 'Notion', description: '搜尋、讀取和更新 Notion 工作區。', category: '知識與文件', setupLabel: '需要登入授權' }, + 'macos-apps': { name: 'macOS 應用程式', description: '透過原生系統權限連接「行事曆」與「提醒事項」。', category: '系統與生產力', setupLabel: undefined }, + 'google-calendar': { name: 'Google 日曆', description: '管理活動、建立會議並檢查可用時間。', category: '系統與生產力', setupLabel: '需要 OAuth 憑證檔案' }, + figma: { name: 'Figma', description: '讀取設計檔案、元件和開發交付資料。', category: '設計與開發', setupLabel: '需要 Personal Access Token' }, + vercel: { name: 'Vercel', description: '檢查專案、部署、記錄和平台文件。', category: '設計與開發', setupLabel: '需要登入授權' }, + supabase: { name: 'Supabase', description: '管理資料庫、專案設定、移轉和 Edge Functions。', category: '設計與開發', setupLabel: '需要登入授權' }, + filesystem: { name: '本機檔案', description: '在選取的資料夾中安全地讀取、寫入和管理檔案。', category: '檔案與知識', setupLabel: '需要選擇允許存取的資料夾' }, + memory: { name: '持久記憶', description: '使用結構化知識圖譜記住實體、關係和重要事實。', category: '檔案與知識', setupLabel: undefined }, + playwright: { name: '瀏覽器自動化', description: '讓 Maka 透過 Playwright 讀取和操作實際網頁。', category: '設計與開發', setupLabel: undefined }, + 'sequential-thinking': { name: '循序思考', description: '為複雜問題提供可修正、可驗證的結構化推理。', category: '推理與規劃', setupLabel: undefined }, + }, + en: { + dingtalk: { name: 'DingTalk', description: 'Manage contacts, calendars, tasks, and collaboration data.', category: 'Communication', setupLabel: 'Requires Client ID and Client Secret' }, + feishu: { name: 'Feishu', description: 'Access Feishu documents, calendars, messages, and OpenAPI.', category: 'Communication', setupLabel: 'Requires App ID and App Secret' }, + slack: { name: 'Slack', description: 'Send messages, manage channels, and collaborate in a Slack workspace.', category: 'Communication', setupLabel: 'Requires Bot Token and Team ID' }, + line: { name: 'LINE', description: 'Send and manage messages through the LINE Bot Messaging API.', category: 'Communication', setupLabel: 'Requires Channel Access Token' }, + notion: { name: 'Notion', description: 'Search, read, and update a Notion workspace.', category: 'Knowledge and documents', setupLabel: 'Requires sign-in authorization' }, + 'macos-apps': { name: 'macOS apps', description: 'Connect Calendar and Reminders through native system permissions.', category: 'System and productivity', setupLabel: undefined }, + 'google-calendar': { name: 'Google Calendar', description: 'Manage events, create meetings, and check availability.', category: 'System and productivity', setupLabel: 'Requires an OAuth credentials file' }, + figma: { name: 'Figma', description: 'Read design files, components, and developer handoff data.', category: 'Design and development', setupLabel: 'Requires a Personal Access Token' }, + vercel: { name: 'Vercel', description: 'Inspect projects, deployments, logs, and platform documentation.', category: 'Design and development', setupLabel: 'Requires sign-in authorization' }, + supabase: { name: 'Supabase', description: 'Manage databases, project configuration, migrations, and Edge Functions.', category: 'Design and development', setupLabel: 'Requires sign-in authorization' }, + filesystem: { name: 'Local files', description: 'Safely read, write, and manage files in selected directories.', category: 'Files and knowledge', setupLabel: 'Requires selecting allowed directories' }, + memory: { name: 'Persistent memory', description: 'Remember entities, relationships, and important facts in a structured knowledge graph.', category: 'Files and knowledge', setupLabel: undefined }, + playwright: { name: 'Browser automation', description: 'Let Maka read and operate real web pages through Playwright.', category: 'Design and development', setupLabel: undefined }, + 'sequential-thinking': { name: 'Sequential thinking', description: 'Provide revisable, verifiable structured reasoning for complex problems.', category: 'Reasoning and planning', setupLabel: undefined }, + }, +} satisfies UiCatalog>; export function getMcpCatalog(locale: UiLocale): McpCatalogEntry[] { - if (locale === 'zh-CN') return MCP_CATALOG; - const copy = locale === 'zh-TW' ? MCP_CATALOG_TRADITIONAL_COPY : MCP_CATALOG_ENGLISH_COPY; + const copy = MCP_CATALOG_COPY[locale]; return MCP_CATALOG.map((entry) => ({ ...entry, ...copy[entry.id] })); } diff --git a/apps/desktop/src/renderer/settings/provider-add-form.tsx b/apps/desktop/src/renderer/settings/provider-add-form.tsx index 1588995847..e47d4fc85e 100644 --- a/apps/desktop/src/renderer/settings/provider-add-form.tsx +++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx @@ -179,6 +179,7 @@ export function AddProviderForm(props: { // a locale in the assertion. function issueMessage(issue: AddProviderIssue): string { if (issue.field === 'slug') { + // zh passes the raw validation detail through by design (#2672 exception) return issue.reason === 'duplicate' ? copy.duplicateSlug : locale === 'zh-CN' diff --git a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx index 49fbb8ac3f..634648f542 100644 --- a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx +++ b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx @@ -49,6 +49,10 @@ import { } from "@maka/ui"; import { Cpu, ICON_SIZE } from "@maka/ui/icons"; import { getSettingsProjectsCopy } from "../locales/settings-projects-copy.js"; + +// Extracted from the catalog getter instead of a new @maka/core/ui-locale +// import: this legacy file's dependency budget is frozen (net-zero rule). +type UiLocale = Parameters[0]; import { PasswordInput } from "./password-input.js"; import { settingsActionErrorMessage } from "./settings-error-copy.js"; import { SettingsField, SettingsRow, SettingsSection } from "./settings-section.js"; @@ -59,7 +63,6 @@ import { } from './runtime-host-management-dialog.js'; import { PeerMeshPeerIdButton, - getRuntimeHostPeerMeshCopy, RuntimeHostAddComputerMenu, RuntimeHostConnectionCodeDialog, RuntimeHostPairingRecoveryButton, @@ -93,7 +96,6 @@ export function RuntimeHostProfilesSection(props: { }) { const locale = useUiLocale(); const copy = getSettingsProjectsCopy(locale).runtimeHost; - const peerMeshCopy = getRuntimeHostPeerMeshCopy(locale); const pairingActionCopy: RuntimeHostPairingActionCopy = { retry: copy.resolvePairingRecovery, retryFailed: copy.resolvePairingRecoveryFailed, @@ -409,10 +411,10 @@ export function RuntimeHostProfilesSection(props: { localAccessEnabling ? copy.remoteAccessEnabling : localAccess?.state === 'unsupported' - ? localAccess.message - : localAccess?.state === 'unavailable' ? localAccess.message - : copy.thisComputerRemoteAccessHelp + : localAccess?.state === 'unavailable' + ? localAccess.message + : copy.thisComputerRemoteAccessHelp } end={( @@ -670,9 +672,9 @@ export function RuntimeHostProfilesSection(props: { settingsActionErrorMessage(error, locale)} className="settingsRuntimeHostPeerId" /> @@ -695,13 +697,11 @@ export function RuntimeHostProfilesSection(props: { ) : null} {entry.peerPath ? ( - + @@ -844,8 +844,9 @@ export function RuntimeHostProfilesSection(props: { function peerPathDetail( path: RuntimeHostPeerConnectionPath, - copy: ReturnType, + locale: UiLocale, ): string { + const copy = getSettingsProjectsCopy(locale).runtimeHost; if (path.kind === 'transit') { return `${copy.peerPathTransit} · ${abbreviatePeerId(path.relayPeerId)}`; } @@ -855,10 +856,23 @@ function peerPathDetail( ? 'QUIC' : path.transport === 'tcp' ? 'TCP' - : copy.peerPathOther; + : copy.peerPathTransportOther; return `${copy.peerPathDirect} · ${transport}`; } +function peerPathLabel( + path: RuntimeHostPeerConnectionPath, + locale: UiLocale, +): string { + const copy = getSettingsProjectsCopy(locale).runtimeHost; + if (path.kind === 'transit') { + return copy.peerPathTransit; + } + if (path.transport === 'webrtc') return 'WebRTC'; + if (path.transport === 'quic') return 'QUIC'; + if (path.transport === 'tcp') return 'TCP'; + return copy.peerPathDirect; +} function abbreviatePeerId(peerId: string): string { return peerId.length <= 20 ? peerId : `${peerId.slice(0, 10)}…${peerId.slice(-6)}`; From 079567fbf827c61c437de70c78c0c170362ade03 Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Thu, 3 Sep 2026 02:19:03 -0700 Subject: [PATCH 2/3] fix(desktop): drop unreachable peer route state copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The peer mesh member projection types state as a closed five-value union, so the routeState catalog's sixth key was new copy no locale could ever render. Its neighbour endpointKind does need an unknown fallback — that field is optional — which is likely where the extra key came from. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_01Tik4Vo8b7oXizLS3ZDFHRn --- apps/desktop/src/renderer/locales/peer-mesh-copy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/locales/peer-mesh-copy.ts b/apps/desktop/src/renderer/locales/peer-mesh-copy.ts index cd76f07b40..5187ac4200 100644 --- a/apps/desktop/src/renderer/locales/peer-mesh-copy.ts +++ b/apps/desktop/src/renderer/locales/peer-mesh-copy.ts @@ -113,7 +113,7 @@ export interface PeerMeshCopy { readonly reservations: string; readonly circuits: string; readonly routeState: Record< - 'local' | 'connecting' | 'reachable' | 'reconnecting' | 'needs_repair' | 'unknown', + 'local' | 'connecting' | 'reachable' | 'reconnecting' | 'needs_repair', string >; readonly endpointKind: Record<'client' | 'host' | 'unknown', string>; @@ -267,7 +267,6 @@ const PEER_MESH_COPY = { reachable: '可达', reconnecting: '正在恢复连接', needs_repair: '需要新邀请码修复', - unknown: '路径未知', }, endpointKind: { client: 'Client', @@ -586,7 +585,6 @@ const PEER_MESH_COPY = { reachable: 'Reachable', reconnecting: 'Reconnecting', needs_repair: 'Needs a new invitation', - unknown: 'Route unknown', }, endpointKind: { client: 'Client', From ec167f5092ffff0218748a77ea62b0ee40479e0d Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Thu, 3 Sep 2026 02:53:28 -0700 Subject: [PATCH 3/3] chore(desktop): simplify mcp catalog overlay Drop the zh Object.fromEntries copy that defeated satisfies checking and reallocated identical entries; zh returns the base entries directly. Generated-by: Muse Spark --- apps/desktop/src/renderer/mcp-catalog.ts | 17 ++++++----------- .../settings/runtime-host-profiles-section.tsx | 2 ++ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/mcp-catalog.ts b/apps/desktop/src/renderer/mcp-catalog.ts index 108d645487..90430648c4 100644 --- a/apps/desktop/src/renderer/mcp-catalog.ts +++ b/apps/desktop/src/renderer/mcp-catalog.ts @@ -17,7 +17,7 @@ * under the License. */ -import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; +import type { UiLocale } from '@maka/core/ui-locale'; import type { McpServerConfig } from '@maka/core/mcp'; export type McpCatalogId = @@ -223,14 +223,8 @@ type McpCatalogLocalizedCopy = Pick [ - entry.id, - { name: entry.name, description: entry.description, category: entry.category, setupLabel: entry.setupLabel }, - ]), - ) as Record, +// zh-CN copy is the base entries; the other locales overlay it. +const MCP_CATALOG_OVERLAY_COPY: Record, Record> = { 'zh-TW': { dingtalk: { name: 'DingTalk', description: '管理聯絡人、行事曆、任務和協作資料。', category: '通訊', setupLabel: '需要 Client ID 和 Client Secret' }, feishu: { name: '飛書', description: '存取飛書文件、行事曆、訊息和 OpenAPI。', category: '通訊', setupLabel: '需要 App ID 和 App Secret' }, @@ -263,10 +257,11 @@ const MCP_CATALOG_COPY = { playwright: { name: 'Browser automation', description: 'Let Maka read and operate real web pages through Playwright.', category: 'Design and development', setupLabel: undefined }, 'sequential-thinking': { name: 'Sequential thinking', description: 'Provide revisable, verifiable structured reasoning for complex problems.', category: 'Reasoning and planning', setupLabel: undefined }, }, -} satisfies UiCatalog>; +}; export function getMcpCatalog(locale: UiLocale): McpCatalogEntry[] { - const copy = MCP_CATALOG_COPY[locale]; + if (locale === 'zh-CN') return MCP_CATALOG; + const copy = MCP_CATALOG_OVERLAY_COPY[locale]; return MCP_CATALOG.map((entry) => ({ ...entry, ...copy[entry.id] })); } diff --git a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx index 634648f542..581327e9f6 100644 --- a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx +++ b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx @@ -52,6 +52,8 @@ import { getSettingsProjectsCopy } from "../locales/settings-projects-copy.js"; // Extracted from the catalog getter instead of a new @maka/core/ui-locale // import: this legacy file's dependency budget is frozen (net-zero rule). +// TODO(#4581): replace with `import type { UiLocale }` once catalog +// type-only imports stop counting toward dependency debt. type UiLocale = Parameters[0]; import { PasswordInput } from "./password-input.js"; import { settingsActionErrorMessage } from "./settings-error-copy.js";