From e6b40a80afd508d947aa2bc2bdc757d63e121a62 Mon Sep 17 00:00:00 2001 From: testikun Date: Thu, 3 Sep 2026 17:49:43 +0800 Subject: [PATCH] feat(i18n-ko): localize desktop slice 2 catalogs --- .../main/__tests__/korean-catalogs.test.ts | 138 ++ .../src/main/computer-use/status-item.ts | 5 + .../src/main/native-diagnostic-dialog-copy.ts | 46 + .../permission-overlay-copy.ts | 19 + .../src/main/runtime-host-quit-copy.ts | 9 + .../src/main/runtime-host-upgrade-copy.ts | 22 + .../settings-provider-copy.ts | 1118 ++++++++++++++--- .../src/renderer/locales/artifact-copy.ts | 105 ++ .../src/renderer/locales/browser-copy.ts | 24 + .../src/renderer/locales/conversation-copy.ts | 524 +++++++- .../locales/external-session-import-copy.ts | 53 + apps/desktop/src/renderer/locales/mcp-copy.ts | 146 +++ .../src/renderer/locales/onboarding-copy.ts | 41 + .../locales/permission-center-copy.ts | 47 + .../src/renderer/locales/plan-mode-copy.ts | 66 + .../locales/session-collaboration-copy.ts | 2 +- .../src/renderer/locales/settings-bot-copy.ts | 979 +++++++++++++-- .../locales/settings-daily-review-copy.ts | 37 +- .../renderer/locales/settings-data-copy.ts | 302 ++++- .../renderer/locales/settings-health-copy.ts | 434 ++++++- .../renderer/locales/settings-memory-copy.ts | 596 ++++++++- .../locales/settings-navigation-copy.ts | 255 +++- .../locales/settings-preferences-copy.ts | 65 +- .../locales/settings-projects-copy.ts | 413 +++++- .../renderer/locales/settings-shared-copy.ts | 58 +- .../locales/settings-subagents-copy.ts | 180 ++- .../renderer/locales/settings-tasks-copy.ts | 65 +- .../locales/settings-test-result-copy.ts | 119 +- .../renderer/locales/settings-usage-copy.ts | 294 ++++- .../locales/settings-web-search-copy.ts | 365 +++++- .../src/renderer/locales/shell-copy.ts | 551 ++++++++ .../renderer/locales/shell-remaining-copy.ts | 2 +- .../src/renderer/settings/bot-chat-detail.tsx | 3 +- .../renderer/settings/bot-chat-overview.tsx | 4 +- .../settings/bot-onboarding-modal.tsx | 3 +- .../settings/daily-review-settings-page.tsx | 4 +- .../settings/provider-catalog-page.tsx | 4 +- .../settings/provider-display-copy.ts | 1065 ++++++++++++++-- .../src/renderer/settings/providers-panel.tsx | 4 +- packages/core/src/__tests__/ui-locale.test.ts | 8 + packages/core/src/relative-time.ts | 1 + packages/core/src/tool-quiet-preview.ts | 10 + packages/core/src/ui-locale.ts | 11 +- packages/ui/src/chat-turn.tsx | 3 +- packages/ui/src/conversation-copy.ts | 172 ++- packages/ui/src/daily-review-copy.ts | 35 + packages/ui/src/locale-helpers.ts | 8 + packages/ui/src/runtime-resume-copy.ts | 17 +- packages/ui/src/scheduled-task-copy.ts | 36 + packages/ui/src/session-hover-card-copy.ts | 12 + packages/ui/src/shared-ui-copy.ts | 84 ++ packages/ui/src/shell-controls-copy.ts | 32 + packages/ui/src/skills-copy.ts | 15 + packages/ui/src/tool-activity/copy.ts | 101 ++ 54 files changed, 7752 insertions(+), 960 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/korean-catalogs.test.ts diff --git a/apps/desktop/src/main/__tests__/korean-catalogs.test.ts b/apps/desktop/src/main/__tests__/korean-catalogs.test.ts new file mode 100644 index 0000000000..181f450100 --- /dev/null +++ b/apps/desktop/src/main/__tests__/korean-catalogs.test.ts @@ -0,0 +1,138 @@ +/* + * 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 assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { getArtifactCopy } from '../../renderer/locales/artifact-copy.js'; +import { getBrowserCopy } from '../../renderer/locales/browser-copy.js'; +import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js'; +import { getExternalSessionImportCopy } from '../../renderer/locales/external-session-import-copy.js'; +import { getMcpCopy } from '../../renderer/locales/mcp-copy.js'; +import { getPlanModeCopy } from '../../renderer/locales/plan-mode-copy.js'; +import { getBotSettingsCopy } from '../../renderer/locales/settings-bot-copy.js'; +import { getDailyReviewSettingsCopy } from '../../renderer/locales/settings-daily-review-copy.js'; +import { getDataSettingsCopy } from '../../renderer/locales/settings-data-copy.js'; +import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js'; +import type { HealthSignal } from '@maka/core/health'; +import { getMemorySettingsCopy } from '../../renderer/locales/settings-memory-copy.js'; +import { getSettingsNavigationCopy } from '../../renderer/locales/settings-navigation-copy.js'; +import { getSettingsProjectsCopy } from '../../renderer/locales/settings-projects-copy.js'; +import { getProviderSettingsCopy } from '../../renderer/features/connection-settings/index.js'; +import { getSettingsSharedCopy } from '../../renderer/locales/settings-shared-copy.js'; +import { getSubagentSettingsCopy } from '../../renderer/locales/settings-subagents-copy.js'; +import { getSettingsTasksCopy } from '../../renderer/locales/settings-tasks-copy.js'; +import { settingsTestResultMessage } from '../../renderer/locales/settings-test-result-copy.js'; +import type { SettingsTestResult } from '@maka/core/settings'; +import { getUsageSettingsCopy } from '../../renderer/locales/settings-usage-copy.js'; +import { getWebSearchSettingsCopy } from '../../renderer/locales/settings-web-search-copy.js'; +import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js'; + +function collectStrings(value: unknown): string[] { + if (typeof value === 'string') return [value]; + if (typeof value === 'function' || value === null || typeof value !== 'object') return []; + return Object.values(value).flatMap(collectStrings); +} + +test('all Desktop slice-2 catalogs expose visibly Korean copy', () => { + const values = [ + getArtifactCopy('ko').pane.copy, + getBrowserCopy('ko').title, + getDesktopConversationCopy('ko').actions.conversationErrorTitle, + getExternalSessionImportCopy('ko').sourceLabel, + getMcpCopy('ko').page.setupTitle, + getPlanModeCopy('ko').proposal.execute, + getBotSettingsCopy('ko').overview.reload, + getDailyReviewSettingsCopy('ko').aria, + getDataSettingsCopy('ko').backupTitle, + getHealthCenterCopy('ko').title, + getMemorySettingsCopy('ko').text.title, + getSettingsNavigationCopy('ko').sections.general.label, + getSettingsProjectsCopy('ko').runtimeHost.title, + getProviderSettingsCopy('ko').detail.modelKey, + getSettingsSharedCopy('ko').modalLabel, + getSubagentSettingsCopy('ko').section.title, + getSettingsTasksCopy('ko').listAria, + getUsageSettingsCopy('ko').details, + getWebSearchSettingsCopy('ko').search, + ]; + + for (const value of values) assert.match(value, /[가-힣]/u); + const proxyDisabled: SettingsTestResult = { ok: false, code: 'proxy_disabled', message: '' }; + assert.match(settingsTestResultMessage(proxyDisabled, 'ko'), /[가-힣]/u); +}); + +test('Korean catalog additions do not leak Korean into English or leave key dynamic copy in English', () => { + const catalogs = [ + getArtifactCopy, + getBrowserCopy, + getDesktopConversationCopy, + getExternalSessionImportCopy, + getMcpCopy, + getPlanModeCopy, + getBotSettingsCopy, + getDailyReviewSettingsCopy, + getDataSettingsCopy, + getHealthCenterCopy, + getMemorySettingsCopy, + getSettingsNavigationCopy, + getSettingsProjectsCopy, + getProviderSettingsCopy, + getSettingsSharedCopy, + getSubagentSettingsCopy, + getSettingsTasksCopy, + getUsageSettingsCopy, + getWebSearchSettingsCopy, + ]; + for (const getCopy of catalogs) { + assert.equal(collectStrings(getCopy('en')).some((value) => /[가-힣]/u.test(value)), false); + } + + assert.match(getMemorySettingsCopy('ko').text.localFile, /[가-힣]/u); + assert.match(getProviderSettingsCopy('ko').panel.loadFailed, /[가-힣]/u); + assert.match(getProviderSettingsCopy('ko').oauthFlow.refreshFailed, /[가-힣]/u); + assert.match(getHealthCenterCopy('ko').signalMessage({ + id: 'connection:test:runtime', + label: '테스트', + scope: 'llm_connection', + layer: 'runtime_probe', + status: 'ok', + source: 'runtime_probe', + checkedAt: 0, + message: '', + } satisfies HealthSignal), /[가-힣]/u); + const inspector = getDesktopConversationCopy('ko').inspector; + assert.equal(inspector.callKind('daily_review'), '일일 검토'); + assert.equal(inspector.permissionDecision('allow'), '허용됨'); + assert.equal(inspector.retries(2), '2회 재시도'); + assert.equal(getMcpCopy('ko').toast.importedDetail(2), '2 서버를 가져왔습니다.'); + assert.equal(getMcpCopy('ko').row.tools(2), '2개 도구'); + assert.equal(getArtifactCopy('ko').preview.externalLinks(2), '이 미리보기에서는 외부 링크가 비활성화되었습니다. · 외부 링크 2개'); + assert.equal(getUsageSettingsCopy('ko').recordCount(2), '2개 기록'); + assert.deepEqual(getSettingsPreferencesCopy('en').personalization.localeOptions.at(-1), ['ko', '한국어']); + assert.deepEqual(getSettingsPreferencesCopy('zh').personalization.localeOptions.at(-1), ['ko', '한국어']); +}); + +test('Korean representative surfaces preserve interpolation', () => { + const plan = getPlanModeCopy('ko'); + const conversation = getDesktopConversationCopy('ko'); + + assert.equal(plan.abandonConfirmation.description('Release plan'), '“Release plan”의 실행 기록은 유지되지만 다시 시작할 수 없습니다.'); + assert.equal(plan.execution.stepCount(2, 3), '2/3단계'); + assert.equal(conversation.actions.branchCreatedDescription('Release branch'), '새 작업: Release branch'); +}); diff --git a/apps/desktop/src/main/computer-use/status-item.ts b/apps/desktop/src/main/computer-use/status-item.ts index 0506a4d5a9..1d1d0daa68 100644 --- a/apps/desktop/src/main/computer-use/status-item.ts +++ b/apps/desktop/src/main/computer-use/status-item.ts @@ -142,6 +142,11 @@ const COPY: UiCatalog = { stopUnnamed: 'Stop Computer Use', empty: 'No Active Sessions', }, + ko: { + stopUsing: (appName) => `${appName} 사용 중지`, + stopUnnamed: 'Computer Use 중지', + empty: '활성 세션 없음', + }, }; function defaultResolveLocale(): UiLocale { diff --git a/apps/desktop/src/main/native-diagnostic-dialog-copy.ts b/apps/desktop/src/main/native-diagnostic-dialog-copy.ts index c671249561..c78b1d98b7 100644 --- a/apps/desktop/src/main/native-diagnostic-dialog-copy.ts +++ b/apps/desktop/src/main/native-diagnostic-dialog-copy.ts @@ -167,6 +167,52 @@ const COPY = { exit: '退出', }, }, + ko: { + dialog: { + copy: '진단 정보 복사', + copyAgain: '다시 복사', + copied: '진단 정보가 복사되었습니다. 문제 보고서에 붙여넣을 수 있습니다.', + copyFailed: '진단 정보를 복사할 수 없습니다.', + }, + fatalStartup: { + title: 'Maka를 시작하지 못했습니다', + message: 'Maka가 시작을 완료하지 못했습니다.', + detail: '시작 중 예기치 않은 오류가 발생했습니다. 진단 정보를 복사하면 자세한 내용을 확인할 수 있습니다.', + exit: '종료', + }, + rendererGone: { + title: 'Maka 복구 필요', + message: 'Maka 인터페이스가 예기치 않게 중지되었습니다.', + detail: '인터페이스만 복구하며 Maka는 다시 시작하지 않습니다. Runtime Host와 실행 중인 작업은 유지됩니다.', + recover: '인터페이스 복구', + exit: '종료', + }, + runtimeHostRecovery: { + title: 'Maka Runtime Host 수정 필요', + message: '이 작업 공간을 관리하는 Runtime Host를 시작할 수 없습니다.', + detail: 'Maka는 이 Desktop에서 선택한 Runtime Host를 수정할 수 있습니다. 작업 공간, Host ID, 자격 증명 및 설정은 유지됩니다.', + activeTasks: 'Host에서 실행 중인 작업이 있을 수 있습니다. 계속하면 먼저 중단한 후 Host를 다시 시작합니다.', + repairFailed: '이전 수정이 완료되지 않았습니다. 진단 정보를 복사하면 자세한 내용을 확인할 수 있습니다.', + repair: 'Runtime Host 수정', + repairAndRestart: '수정 후 Host 다시 시작', + exit: '종료', + }, + defaultRuntimeHostRecovery: { + title: '기본 Runtime Host에 연결할 수 없습니다', + connectFailed: (profileName) => `${profileName}에 연결할 수 없습니다`, + detail: '다시 시도하거나 Local을 기본 Host로 사용하거나 현재 선택을 유지하고 나중에 설정에서 처리할 수 있습니다.', + retry: '다시 시도', + useLocal: 'Local 사용', + keepOffline: '오프라인 유지', + }, + storageRootRepair: { + title: 'Maka 작업 공간 수정 필요', + message: 'Maka가 이 작업 공간을 확인할 수 없습니다.', + detail: (workspaceRoot) => `시스템의 디스크 ID가 변경되었을 수 있습니다. 복사된 작업 공간이 아닌 이 컴퓨터의 원래 Maka 작업 공간인 경우에만 수정하세요.\n\n${workspaceRoot}`, + repair: '작업 공간 수정', + exit: '종료', + }, + }, } satisfies UiCatalog; export function getNativeDiagnosticDialogCopy(locale: UiLocale): NativeDiagnosticDialogCopy { diff --git a/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts b/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts index afb7814138..eb3fb0faaa 100644 --- a/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts +++ b/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts @@ -88,6 +88,25 @@ const COPY: Catalog = { noBundle: 'Not running from a .app bundle, so there is nothing to drag. Add it manually in System Settings.', }, }, + ko: { + accessibility: { + headline: (appName) => `${appName}을(를) 위 목록으로 드래그하여 손쉬운 사용을 허용하세요`, + fallback: '또는 시스템 설정에서 +를 클릭하고 응용 프로그램에서 앱을 선택하세요.', + granted: '손쉬운 사용 활성화됨', + dismiss: '닫기', + dragHint: '여기로 드래그', + noBundle: '.app 번들에서 실행되지 않아 드래그할 수 없습니다. 시스템 설정에서 수동으로 추가하세요.', + }, + screen_recording: { + headline: (appName) => `${appName}을(를) 위 목록으로 드래그하여 화면 기록을 허용하세요`, + fallback: '또는 시스템 설정에서 +를 클릭하고 응용 프로그램에서 앱을 선택하세요.', + granted: '화면 기록 활성화됨', + dismiss: '닫기', + dragHint: '여기로 드래그', + restartHint: '여전히 거부됨으로 표시되면 앱을 다시 시작하세요. macOS가 이전 거부 결과를 캐시합니다.', + noBundle: '.app 번들에서 실행되지 않아 드래그할 수 없습니다. 시스템 설정에서 수동으로 추가하세요.', + }, + }, }; export function getPermissionOverlayCopy( diff --git a/apps/desktop/src/main/runtime-host-quit-copy.ts b/apps/desktop/src/main/runtime-host-quit-copy.ts index f9bfff703d..e22fdf612b 100644 --- a/apps/desktop/src/main/runtime-host-quit-copy.ts +++ b/apps/desktop/src/main/runtime-host-quit-copy.ts @@ -72,4 +72,13 @@ const COPY = { cause: '原因', button: '好', }, + ko: { + title: 'Maka를 안전하게 종료할 수 없습니다', + message: '로컬 Runtime Host를 안전하게 중지하지 못했습니다. Maka는 계속 실행 중입니다.', + detail: '종료가 취소되었습니다. 다시 시도하거나 문제가 계속되면 진단 정보를 확인하세요.', + process: (pid: number) => `Runtime Host 프로세스 PID: ${pid}`, + manual: '다시 시도해도 실패하면 보존해야 할 실행이 없는지 확인한 후 운영 체제의 프로세스 관리 도구로 이 PID를 중지하세요.', + cause: '원인', + button: '확인', + }, } as const; diff --git a/apps/desktop/src/main/runtime-host-upgrade-copy.ts b/apps/desktop/src/main/runtime-host-upgrade-copy.ts index 53fd212d68..72de991cc4 100644 --- a/apps/desktop/src/main/runtime-host-upgrade-copy.ts +++ b/apps/desktop/src/main/runtime-host-upgrade-copy.ts @@ -184,4 +184,26 @@ const UPGRADE_COPY = { resource: 'Runtime 资源', graph: 'Agent Graph', other: '其他后台活动', }, }, + ko: { + title: '이전 Runtime Host가 실행 중입니다', + message: '다른 Runtime Host 프로세스가 이 작업 공간을 사용하고 있습니다.', + restart: 'Runtime Host 다시 시작', + replace: 'Host 중지 후 계속', + wait: '대기', + cancel: '시작 취소', + uptime: (n: number) => `약 ${n}분 동안 실행됨`, + connections: (n: number) => `${n}개의 다른 클라이언트가 아직 연결되어 있습니다`, + operations: (n: number) => `${n}개의 작업이 실행 중입니다`, + idleNotVerified: '안전한 교체 확인 중 Host가 유휴 상태인지 확인할 수 없습니다.', + unknownActivity: '이 Host 버전은 백그라운드 활동을 보고할 수 없습니다.', + processId: (pid: number) => `프로세스 ID(PID): ${pid}`, + restartWarning: '다시 시작하면 영구 상태는 유지되지만 진행 중인 외부 작업이 중단될 수 있습니다.', + replaceWarning: '중지하면 영구 상태는 유지되지만 진행 중인 외부 작업이 중단될 수 있습니다.', + replaceExplanation: 'Maka가 이 Host를 중지하고 안전하게 교체한 후 시작을 계속합니다.', + exitOwner: (pid: number) => `이 Host를 안전하게 교체하려면 시스템 프로세스 관리자에서 프로세스 ${pid}를 종료하세요.`, + waitExplanation: '기다리면 이 Host가 종료된 후 Maka가 자동으로 계속합니다.', + activity: { + goal: '목표', scheduledTask: '예약된 작업', dailyReview: '일일 검토', execution: '활성 실행', resource: '런타임 리소스', graph: '에이전트 그래프', other: '기타 백그라운드 활동', + }, + }, } as const; diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index ad35d5a432..c787b159c2 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -18,12 +18,7 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - -type WidenCopy = T extends string - ? string - : T extends (...args: infer Args) => string - ? (...args: Args) => string - : { [K in keyof T]: WidenCopy }; +type WidenCopy = T extends string ? string : T extends ((...args: infer Args) => string) ? (...args: Args) => string : { [K in keyof T]: WidenCopy }; // Capability-section strings for the connection detail page — the add-provider // form deliberately carries no declaration controls (capabilities are edited @@ -37,8 +32,7 @@ const zhCapabilitiesCopy = { thinkingBulk: '批量设置思考档位', thinkingBulkHelp: '勾选写入下方全部已启用模型,取消勾选则从全部模型移除;其余档位不受影响。', thinkingBulkTrigger: '应用到全部模型', - thinkingBulkCoverage: (declared: number, total: number) => - declared === 0 ? '全部未声明' : `${declared}/${total} 个模型`, + thinkingBulkCoverage: (declared: number, total: number) => declared === 0 ? '全部未声明' : `${declared}/${total} 个模型`, visionInput: '视觉输入(vision)', visionInputHelp: '「自动」跟随内置元数据;「启用/禁用」是显式声明,覆盖自动判断。', visionAuto: '自动', @@ -50,7 +44,7 @@ const zhCapabilitiesCopy = { fastMode: 'Fast 模式', fastModeHelp: '使用 OpenAI 的 fast service tier;留空跟随服务商默认值。', fastAuto: '自动', - fastEnabled: 'Fast', + fastEnabled: 'Fast' }; const enCapabilitiesCopy = { capabilities: 'Capabilities', @@ -59,11 +53,9 @@ const enCapabilitiesCopy = { thinkingUndeclared: 'Undeclared', thinkingSelectedCount: (count: number) => `${count} selected`, thinkingBulk: 'Set thinking levels for all models', - thinkingBulkHelp: - 'Ticking adds the level to every enabled model below; unticking removes it from all of them. Other levels are left alone.', + thinkingBulkHelp: 'Ticking adds the level to every enabled model below; unticking removes it from all of them. Other levels are left alone.', thinkingBulkTrigger: 'Apply to all models', - thinkingBulkCoverage: (declared: number, total: number) => - declared === 0 ? 'On no model' : `On ${declared} of ${total} models`, + thinkingBulkCoverage: (declared: number, total: number) => declared === 0 ? 'On no model' : `On ${declared} of ${total} models`, visionInput: 'Vision input', visionInputHelp: 'Auto follows built-in metadata; Enabled/Disabled overrides it explicitly.', visionAuto: 'Auto', @@ -75,30 +67,54 @@ const enCapabilitiesCopy = { fastMode: 'Fast mode', fastModeHelp: "Use OpenAI's fast service tier; empty follows the provider default.", fastAuto: 'Auto', - fastEnabled: 'Fast', + fastEnabled: 'Fast' }; - const zhCopy = { detail: { - delete: '删除', cancel: '取消', deleteUnused: '不再需要,删除连接', - deleteFailed: '删除模型连接失败', credentialReadFailed: '读取模型凭据状态失败', refreshFailed: '刷新模型连接失败', - saveFailed: '保存模型连接失败', saveModelsFailed: '保存启用模型失败', - modelKey: '模型密钥', pasteModelKey: '粘贴模型密钥', getModelKey: '获取模型密钥', saving: '保存中…', - advancedRequest: '高级请求设置', advancedRequestHelp: '为这个连接的 HTTP 请求添加请求头和额外 JSON 请求体。请求头值作为凭据保存在本机。', - requestHeaders: '自定义请求头', headerName: '请求头名称', headerValue: '请求头值', retainedHeaderValue: '保留已保存的值', addHeader: '添加请求头', removeHeader: '移除', noRequestHeaders: '未设置自定义请求头。', - extraRequestBody: '额外请求体(JSON)', extraRequestBodyHelp: '仅添加顶层字段;若与 Maka 生成的字段重名,请求会明确失败。', requestCustomizationInvalid: '请求设置无效', requestHeadersInvalidDetail: '请检查请求头名称和值。', requestBodyInvalidDetail: '请输入符合要求的 JSON 对象。', saveAdvancedRequest: '保存高级请求设置', configuredHeaders: (count: number) => `${count} 个请求头`, noAdvancedRequest: '未设置', - oauthLoggedIn: 'OAuth 已登录', oauthLoading: 'OAuth 状态读取中', oauthUnknown: 'OAuth 状态未知', oauthWaiting: '等待 OAuth 登录', + delete: '删除', + cancel: '取消', + deleteUnused: '不再需要,删除连接', + deleteFailed: '删除模型连接失败', + credentialReadFailed: '读取模型凭据状态失败', + refreshFailed: '刷新模型连接失败', + saveFailed: '保存模型连接失败', + saveModelsFailed: '保存启用模型失败', + modelKey: '模型密钥', + pasteModelKey: '粘贴模型密钥', + getModelKey: '获取模型密钥', + saving: '保存中…', + advancedRequest: '高级请求设置', + advancedRequestHelp: '为这个连接的 HTTP 请求添加请求头和额外 JSON 请求体。请求头值作为凭据保存在本机。', + requestHeaders: '自定义请求头', + headerName: '请求头名称', + headerValue: '请求头值', + retainedHeaderValue: '保留已保存的值', + addHeader: '添加请求头', + removeHeader: '移除', + noRequestHeaders: '未设置自定义请求头。', + extraRequestBody: '额外请求体(JSON)', + extraRequestBodyHelp: '仅添加顶层字段;若与 Maka 生成的字段重名,请求会明确失败。', + requestCustomizationInvalid: '请求设置无效', + requestHeadersInvalidDetail: '请检查请求头名称和值。', + requestBodyInvalidDetail: '请输入符合要求的 JSON 对象。', + saveAdvancedRequest: '保存高级请求设置', + configuredHeaders: (count: number) => `${count} 个请求头`, + noAdvancedRequest: '未设置', + oauthLoggedIn: 'OAuth 已登录', + oauthLoading: 'OAuth 状态读取中', + oauthUnknown: 'OAuth 状态未知', + oauthWaiting: '等待 OAuth 登录', oauthLoggedInDetail: '该模型连接使用主进程保存的 OAuth access token;若请求提示需要重新登录,请到账号连接重新授权。', oauthLoadingDetail: '正在读取本机 OAuth 登录状态,读取完成前不会把未知状态显示成未登录。', oauthUnknownDetail: '暂时无法读取本机 OAuth 登录状态;请刷新页面或重新打开设置。', oauthWaitingDetail: '请到账号连接完成登录;登录成功后会自动出现在模型连接里。', oauthRetired: '此登录方式已停用', - oauthRetiredDetail: - '这条连接使用的登录方式已从 Maka 移除,无法再登录,也无法用于对话。改用 Anthropic API Key 连接即可继续使用 Claude 模型;删除这条连接会一并清除本机保存的登录凭据。', + oauthRetiredDetail: '这条连接使用的登录方式已从 Maka 移除,无法再登录,也无法用于对话。改用 Anthropic API Key 连接即可继续使用 Claude 模型;删除这条连接会一并清除本机保存的登录凭据。', credentialLoadingDetail: '正在读取模型凭据状态,读取完成前暂不测试连接或刷新模型。', credentialUnknownDetail: '模型凭据状态暂时没刷新成功,已避免把未知状态显示成未登录或未配置。', testConnection: '测试连接', - updateModels: '更新模型目录', endpoint: '服务地址', + updateModels: '更新模型目录', + endpoint: '服务地址', connectionName: '名称', connectionNamePlaceholder: '给这个连接起个名字', addModel: '添加模型', @@ -111,7 +127,9 @@ const zhCopy = { addModelContextWindow: '上下文窗口', addModelContextWindowHelp: '服务商模型页给出的最大 token 数。缺少它 Maka 只能按 32k 处理,长对话会被提前截断。', addModelContextWindowRequired: '请填写上下文窗口。', - credentials: '连接', dangerZone: '删除连接', deleteRowHelp: '此操作不可撤销。', + credentials: '连接', + dangerZone: '删除连接', + deleteRowHelp: '此操作不可撤销。', credentialsHelp: '密钥只保存在本机。', credentialsHelpAccount: '登录令牌只保存在本机。', modelManagementHelp: '这些模型会出现在任务的模型选择器里。', @@ -119,147 +137,327 @@ const zhCopy = { capabilitiesHelp: '声明每个已启用模型的思考档位、视觉与上下文窗口;保存后生效。', // Row affordances (settings-sidebar 的 InfoRow / ExpandableRow 语言):一行 // 只报状态,改的时候才展开成输入框。 - change: '更换', set: '设置', edit: '编辑', save: '保存', + change: '更换', + set: '设置', + edit: '编辑', + save: '保存', endpointManaged: '由账号登录或服务商管理', endpointMissing: '尚未配置服务地址', endpointModelOverridesNote: '部分模型会通过其他服务端点发送请求', endpointCredentialsMasked: '已保存的地址内嵌凭据,编辑时默认隐藏', modelManagement: '模型', disconnectAndDelete: '退出账号并删除', - login: '登录', loggingIn: '登录中…', relogin: '重新登录', - oauthReloginDetail: '若请求提示需要重新登录,点这里重新走一遍授权。', deviceCode: '登录码:', + copilotImportFailed: '导入 GitHub Copilot 登录失败', + copilotLoggedIn: 'GitHub Copilot 已登录', + copilotWaiting: '等待兼容 GitHub 凭据', + copilotLoggedInDetail: '若账号或组织策略变化,可重新导入兼容凭据。', + copilotWaitingDetail: '配置具有 Copilot Requests 权限的凭据后从本机安全导入。', + reimport: '重新导入', + importCredential: '导入兼容凭据', + login: '登录', + loggingIn: '登录中…', + relogin: '重新登录', + oauthReloginDetail: '若请求提示需要重新登录,点这里重新走一遍授权。', oauthStartDetail: '点下方按钮打开浏览器完成登录,授权成功后会自动刷新这里的状态。', enabledModels: '启用的模型', - searchModels: '搜索模型', selectAllModels: '全部启用', + searchModels: '搜索模型', + selectAllModels: '全部启用', noModels: '暂无可选模型,请先更新模型目录。', - keySet: '已设置', statusLoading: '正在读取状态', credentialUnknown: '凭据状态未知', keyMissing: '尚未设置密钥', - keyTroubleshooting: '模型密钥 / 服务地址 / 代理设置', endpointTroubleshooting: '本地服务 / 服务地址 / 代理设置', oauthTroubleshooting: 'OAuth 登录 / 代理设置', + keySet: '已设置', + statusLoading: '正在读取状态', + credentialUnknown: '凭据状态未知', + keyMissing: '尚未设置密钥', + keyTroubleshooting: '模型密钥 / 服务地址 / 代理设置', + endpointTroubleshooting: '本地服务 / 服务地址 / 代理设置', + oauthTroubleshooting: 'OAuth 登录 / 代理设置', unknownDescription: (provider: string) => `该连接使用的 provider「${provider}」在当前版本未注册。配置和凭据会保留,切回支持它的版本即可继续使用。`, deleteProviderTitle: (name: string) => `删除供应商 ${name}?`, deleteConnectionTitle: (name: string) => `删除模型连接 ${name}?`, deleteUnknownDescription: '删除后,支持该 provider 的其他版本也无法恢复这条连接及其凭据。', - deleteDescription: (isDefault: boolean, disconnectsAccount: boolean) => [ - disconnectsAccount - ? '这会退出本机账号、删除 OAuth 凭据和模型连接;刷新后不会自动重新创建。' - : '这会删除模型连接及其本机凭据;如需再次使用,需要重新添加。', - isDefault - ? '它当前是默认连接;删除后默认模型会变成未设置,已有任务可能需要重新选择模型。' - : '', - ].filter(Boolean).join(' '), - connectionSuccess: (name: string) => `连接成功 · ${name}`, connectionFailed: (name: string) => `连接失败 · ${name}`, + deleteDescription: (isDefault: boolean, disconnectsAccount: boolean) => [disconnectsAccount ? '这会退出本机账号、删除 OAuth 凭据和模型连接;刷新后不会自动重新创建。' : '这会删除模型连接及其本机凭据;如需再次使用,需要重新添加。', isDefault ? '它当前是默认连接;删除后默认模型会变成未设置,已有任务可能需要重新选择模型。' : ''].filter(Boolean).join(' '), + connectionSuccess: (name: string) => `连接成功 · ${name}`, + connectionFailed: (name: string) => `连接失败 · ${name}`, connectionFallbackTitle: (name: string) => `连接可用 · ${name}`, - connectionFallbackDetail: (selected: readonly string[], tested: string) => - `你选择的 ${selected.join('、')} 当前未响应,已改用 ${tested} 验证连接可用;任务中继续使用你选择的模型可能会失败。`, - connectionTestError: (name: string) => `连接测试出错 · ${name}`, modelsFetched: (count: number, name: string) => `已拉取 ${count} 个模型 · ${name}`, + connectionFallbackDetail: (selected: readonly string[], tested: string) => `你选择的 ${selected.join('、')} 当前未响应,已改用 ${tested} 验证连接可用;任务中继续使用你选择的模型可能会失败。`, + connectionTestError: (name: string) => `连接测试出错 · ${name}`, + modelsFetched: (count: number, name: string) => `已拉取 ${count} 个模型 · ${name}`, modelsFetchFailed: (name: string) => `拉取模型失败 · ${name}`, modelsFetchFailedDetail: (message: string, troubleshooting: string) => `${message} · 当前继续显示静态列表,请确认 ${troubleshooting} 后重试。`, - authTroubleshooting: (value: string) => `鉴权失败,请确认 ${value} 后重试。`, recheckTroubleshooting: (value: string) => `检查 ${value} 后重试。`, - modelKeyAria: (name: string) => `${name} 模型密钥`, + authTroubleshooting: (value: string) => `鉴权失败,请确认 ${value} 后重试。`, + recheckTroubleshooting: (value: string) => `检查 ${value} 后重试。`, + modelKeyAria: (name: string) => `${name} 模型密钥` }, shared: { - actionFallback: '模型连接服务暂时不可用,请稍后重试。', rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', - timeout: '请求超时,请检查网络或代理后重试。', unavailable: '模型服务暂时不可用,请稍后重试。', - network: '网络错误,请检查服务地址或代理设置后重试。', statusUnavailable: '连接测试状态暂时无法显示,请重新测试。', - categories: { oauth: 'OAuth', domestic: '国内', overseas: '海外', local: '本地', custom: 'Custom' }, - connectionStatuses: { retired: '已停用 · 请删除', reauth: '需要重新登录', disabledFailed: '暂不可用 · 上次连接失败', disabled: '暂不可用', failed: '上次连接失败' }, - lastTest: { - '连接已验证': '连接已验证', '鉴权失败': '鉴权失败', '请求超时': '请求超时', '网络错误': '网络错误', '模型服务返回错误': '模型服务返回错误', '连接测试失败': '连接测试失败', - 'connection verified': '连接已验证', 'authentication failed': '鉴权失败', 'request timed out': '请求超时', 'network error': '网络错误', 'provider returned an error': '模型服务返回错误', 'connection test failed': '连接测试失败', - 'claude oauth 未登录。': 'Claude OAuth 未登录。', 'claude oauth 本地凭据读取失败。': 'Claude OAuth 本地凭据读取失败。', 'claude oauth 需要重新登录。': 'Claude OAuth 需要重新登录。', 'claude oauth 已登录。': 'Claude OAuth 已登录。', 'claude oauth 已退出登录。': 'Claude OAuth 已退出登录。', - 'codex oauth 未登录。': 'Codex OAuth 未登录。', 'codex oauth 本地凭据读取失败。': 'Codex OAuth 本地凭据读取失败。', 'codex oauth 需要重新登录。': 'Codex OAuth 需要重新登录。', 'codex oauth 已登录。': 'Codex OAuth 已登录。', 'codex oauth 已退出登录。': 'Codex OAuth 已退出登录。', - '当前账号无可用 codex 模型。': '当前账号无可用 Codex 模型。', 'codex 模型列表获取失败。': 'Codex 模型列表获取失败。', - 'github copilot 需要重新导入 github cli 登录。': 'GitHub Copilot 需要重新导入 GitHub CLI 登录。', 'github copilot 无法读取当前账号可用模型,请重新验证登录。': 'GitHub Copilot 无法读取当前账号可用模型,请重新验证登录。', 'github copilot 登录已导入。': 'GitHub Copilot 登录已导入。', 'github copilot 连接未能保存,请重新导入登录。': 'GitHub Copilot 连接未能保存,请重新导入登录。', 'github copilot 已移除本地登录。': 'GitHub Copilot 已移除本地登录。', + actionFallback: '模型连接服务暂时不可用,请稍后重试。', + rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', + timeout: '请求超时,请检查网络或代理后重试。', + unavailable: '模型服务暂时不可用,请稍后重试。', + network: '网络错误,请检查服务地址或代理设置后重试。', + statusUnavailable: '连接测试状态暂时无法显示,请重新测试。', + categories: { + oauth: 'OAuth', + domestic: '国内', + overseas: '海外', + local: '本地', + custom: 'Custom' }, + connectionStatuses: { + retired: '已停用 · 请删除', + reauth: '需要重新登录', + disabledFailed: '暂不可用 · 上次连接失败', + disabled: '暂不可用', + failed: '上次连接失败' + }, + lastTest: { + '连接已验证': '连接已验证', + '鉴权失败': '鉴权失败', + '请求超时': '请求超时', + '网络错误': '网络错误', + '模型服务返回错误': '模型服务返回错误', + '连接测试失败': '连接测试失败', + 'connection verified': '连接已验证', + 'authentication failed': '鉴权失败', + 'request timed out': '请求超时', + 'network error': '网络错误', + 'provider returned an error': '模型服务返回错误', + 'connection test failed': '连接测试失败', + 'claude oauth 未登录。': 'Claude OAuth 未登录。', + 'claude oauth 本地凭据读取失败。': 'Claude OAuth 本地凭据读取失败。', + 'claude oauth 需要重新登录。': 'Claude OAuth 需要重新登录。', + 'claude oauth 已登录。': 'Claude OAuth 已登录。', + 'claude oauth 已退出登录。': 'Claude OAuth 已退出登录。', + 'codex oauth 未登录。': 'Codex OAuth 未登录。', + 'codex oauth 本地凭据读取失败。': 'Codex OAuth 本地凭据读取失败。', + 'codex oauth 需要重新登录。': 'Codex OAuth 需要重新登录。', + 'codex oauth 已登录。': 'Codex OAuth 已登录。', + 'codex oauth 已退出登录。': 'Codex OAuth 已退出登录。', + '当前账号无可用 codex 模型。': '当前账号无可用 Codex 模型。', + 'codex 模型列表获取失败。': 'Codex 模型列表获取失败。', + 'github copilot 需要重新导入 github cli 登录。': 'GitHub Copilot 需要重新导入 GitHub CLI 登录。', + 'github copilot 无法读取当前账号可用模型,请重新验证登录。': 'GitHub Copilot 无法读取当前账号可用模型,请重新验证登录。', + 'github copilot 登录已导入。': 'GitHub Copilot 登录已导入。', + 'github copilot 连接未能保存,请重新导入登录。': 'GitHub Copilot 连接未能保存,请重新导入登录。', + 'github copilot 已移除本地登录。': 'GitHub Copilot 已移除本地登录。' + } }, panel: { - tabs: { all: '全部', recommended: '推荐', accounts: '账号', plans: '模型计划', api: 'API', aggregators: '聚合服务', local: '本地' }, - loadFailed: '载入模型连接失败', loadingAria: '正在加载模型供应商', connections: '模型连接', - retry: '点击重试。', empty: '还没有模型连接', connectionRemoved: '原连接已被删除或移除,已返回模型连接列表。', - connectedLoading: '连接已添加,正在载入详情…', connectedLoadFailed: '连接已添加,但暂时无法刷新连接列表。', + tabs: { + all: '全部', + recommended: '推荐', + accounts: '账号', + plans: '模型计划', + api: 'API', + aggregators: '聚合服务', + local: '本地' + }, + loadFailed: '载入模型连接失败', + loadingAria: '正在加载模型供应商', + connections: '模型连接', + retry: '点击重试。', + empty: '还没有模型连接', + connectionRemoved: '原连接已被删除或移除,已返回模型连接列表。', + connectedLoading: '连接已添加,正在载入详情…', + connectedLoadFailed: '连接已添加,但暂时无法刷新连接列表。', connectionIdentityChanged: '新连接的身份与登录结果不一致,请返回连接列表后重试。', - emptyHelp: '从下方选择一种连接方式开始。', default: '默认', setDefault: '设为默认', setDefaultTitle: '让新任务默认使用这个连接', setDefaultPending: '设置中…', setDefaultFailed: '设为默认失败', addHelp: '选择账号登录、模型计划、API、聚合服务或本地运行时。', - categoriesAria: '模型供应商分类', searchPlaceholder: '搜索服务商', searchAria: '搜索模型服务商', noMatch: '没有匹配的服务商', clearSearch: '清除搜索', - createSubtitle: '完成必要配置后,连接会出现在模型页上方。', connection: '模型连接', - count: (value: number) => `· ${value}`, connectTitle: (name: string) => `连接 ${name}`, + emptyHelp: '从下方选择一种连接方式开始。', + default: '默认', + setDefault: '设为默认', + setDefaultTitle: '让新任务默认使用这个连接', + setDefaultPending: '设置中…', + setDefaultFailed: '设为默认失败', + addHelp: '选择账号登录、模型计划、API、聚合服务或本地运行时。', + categoriesAria: '模型供应商分类', + searchPlaceholder: '搜索服务商', + searchAria: '搜索模型服务商', + noMatch: '没有匹配的服务商', + clearSearch: '清除搜索', + createSubtitle: '完成必要配置后,连接会出现在模型页上方。', + connection: '模型连接', + count: (value: number) => `· ${value}`, + connectTitle: (name: string) => `连接 ${name}`, chipAria: (name: string, provider: string, isDefault: boolean, status?: string) => `模型连接:${name},供应商:${provider}${isDefault ? ',默认连接' : ''}${status ? `,${status}` : ''}`, - addConnection: '添加连接', category: '分类', backToList: '返回模型连接', backToCatalog: '返回服务商列表', + addConnection: '添加连接', + category: '分类', + backToList: '返回模型连接', + backToCatalog: '返回服务商列表' }, catalog: { unavailable: '未开放', - cardAria: (name: string, description: string) => `添加模型供应商:${name},${description}`, + cardAria: (name: string, description: string) => `添加模型供应商:${name},${description}` }, add: { - invalidSlug: '连接标识格式不正确', duplicateSlug: '连接标识已存在', cloudflareAccount: '请填写 Cloudflare Account ID', endpointRequired: '这个供应商需要填写服务地址', + invalidSlug: '连接标识格式不正确', + duplicateSlug: '连接标识已存在', + cloudflareAccount: '请填写 Cloudflare Account ID', + endpointRequired: '这个供应商需要填写服务地址', accountLogin: '请到账号连接完成登录;登录成功后会自动创建模型连接。', - apiKeyPlaceholder: '输入或粘贴 API Key', cancel: '取消', accountTitle: '使用账号连接登录', - advancedRequest: '高级请求设置', expandAdvancedRequest: '展开高级请求设置', collapseAdvancedRequest: '收起高级请求设置', - requestHeaders: '自定义请求头', headerName: '请求头名称', headerValue: '请求头值', retainedHeaderValue: '保留已保存的值', addHeader: '添加请求头', removeHeader: '移除', noRequestHeaders: '未设置自定义请求头。', - extraRequestBody: '额外请求体(JSON)', extraRequestBodyHelp: '仅添加顶层字段;与 Maka 生成字段重名时会明确失败。请求头值将作为凭据保存在本机。', requestCustomizationInvalid: '请检查高级请求设置。', + apiKeyPlaceholder: '输入或粘贴 API Key', + cancel: '取消', + accountTitle: '使用账号连接登录', + advancedRequest: '高级请求设置', + expandAdvancedRequest: '展开高级请求设置', + collapseAdvancedRequest: '收起高级请求设置', + requestHeaders: '自定义请求头', + headerName: '请求头名称', + headerValue: '请求头值', + retainedHeaderValue: '保留已保存的值', + addHeader: '添加请求头', + removeHeader: '移除', + noRequestHeaders: '未设置自定义请求头。', + extraRequestBody: '额外请求体(JSON)', + extraRequestBodyHelp: '仅添加顶层字段;与 Maka 生成字段重名时会明确失败。请求头值将作为凭据保存在本机。', + requestCustomizationInvalid: '请检查高级请求设置。', accountDetail: '不要在这里手动添加;请回到模型连接页的账号连接完成登录,Maka 会自动创建并刷新模型连接。', - slug: '连接标识', name: '显示名称', + slug: '连接标识', + name: '显示名称', accountIdPlaceholder: '填写账户 ID', - saving: '保存中…', save: '保存供应商', keyRequired: (name: string) => `请填写 ${name} API Key`, - apiKeyLabel: 'API Key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: '服务地址', - defaultModel: '默认模型', defaultModelPlaceholder: '留空即可,保存后自动拉取', defaultModelHelp: '保存后 Maka 会向该端点拉取模型目录。只有当端点不提供目录时,才需要在这里手填一个模型 ID。', - onboardingVerifyAndChoose: '验证并选择模型', onboardingVerifying: '正在验证密钥并获取模型…', - onboardingChooseModels: '选择此连接使用的模型', onboardingChooseModelsHelp: '添加后仍可在连接详情中启用其他模型。', onboardingEnabledModels: '启用的模型', onboardingSearchModels: '搜索模型', onboardingAddConnection: '添加连接', onboardingBack: '返回修改', - onboardingAuthFailed: '密钥验证失败,请检查后重试。', onboardingTimeout: '验证超时,请检查网络或代理后重试。', onboardingNetwork: '无法连接模型服务,请检查网络、代理或服务地址。', onboardingUnavailable: '模型连接服务暂时不可用,请稍后重试。', onboardingInvalidResponse: '模型服务返回了无法识别的结果,请稍后重试。', onboardingCatalogFull: '连接数量已达上限,请先删除不再使用的连接。', onboardingModelsChanged: '可用模型已发生变化,请重新验证后选择。', onboardingNoModels: '没有发现可用模型,当前未创建连接。', onboardingSelectModel: '请至少启用一个模型。', - onboardingOutcomeUnknown: '保存结果暂时无法确认', onboardingOutcomeUnknownDetail: '请勿再次添加,以免创建重复连接。重新加载连接列表并检查该连接是否已经出现;仍不确定时请先重连 Runtime Host。', onboardingReloadConnections: '重新加载连接列表', onboardingRestart: '仍要添加另一个连接', - ...zhCapabilitiesCopy, + saving: '保存中…', + save: '保存供应商', + keyRequired: (name: string) => `请填写 ${name} API Key`, + apiKeyLabel: 'API Key', + accountIdLabel: 'Cloudflare Account ID', + endpointLabel: '服务地址', + defaultModel: '默认模型', + defaultModelPlaceholder: '留空即可,保存后自动拉取', + defaultModelHelp: '保存后 Maka 会向该端点拉取模型目录。只有当端点不提供目录时,才需要在这里手填一个模型 ID。', + onboardingVerifyAndChoose: '验证并选择模型', + onboardingVerifying: '正在验证密钥并获取模型…', + onboardingChooseModels: '选择此连接使用的模型', + onboardingChooseModelsHelp: '添加后仍可在连接详情中启用其他模型。', + onboardingEnabledModels: '启用的模型', + onboardingSearchModels: '搜索模型', + onboardingAddConnection: '添加连接', + onboardingBack: '返回修改', + onboardingAuthFailed: '密钥验证失败,请检查后重试。', + onboardingTimeout: '验证超时,请检查网络或代理后重试。', + onboardingNetwork: '无法连接模型服务,请检查网络、代理或服务地址。', + onboardingUnavailable: '模型连接服务暂时不可用,请稍后重试。', + onboardingInvalidResponse: '模型服务返回了无法识别的结果,请稍后重试。', + onboardingCatalogFull: '连接数量已达上限,请先删除不再使用的连接。', + onboardingModelsChanged: '可用模型已发生变化,请重新验证后选择。', + onboardingNoModels: '没有发现可用模型,当前未创建连接。', + onboardingSelectModel: '请至少启用一个模型。', + onboardingOutcomeUnknown: '保存结果暂时无法确认', + onboardingOutcomeUnknownDetail: '请勿再次添加,以免创建重复连接。重新加载连接列表并检查该连接是否已经出现;仍不确定时请先重连 Runtime Host。', + onboardingReloadConnections: '重新加载连接列表', + onboardingRestart: '仍要添加另一个连接', + ...zhCapabilitiesCopy }, oauthFlow: { - refreshFailed: '刷新登录状态失败', accountActionFailed: (name: string) => `${name} 账号操作失败`, loginFailedRetry: '登录失败,请稍后重试。', - retry: '请稍后再试。', startFailed: '无法开始登录', startFailedRetry: '无法开始登录,请稍后再试。', openFailed: '无法打开浏览器', - openFailedRetry: '无法打开浏览器,请稍后重试。', loginSuccess: '登录成功', bound: (name: string) => `${name} 已绑定本机。`, - incomplete: '登录未完成', incompleteRetry: '登录未完成,请重新打开浏览器授权。', loginFailed: '登录失败', - logoutDescription: '将删除本机保存的订阅凭据,之后需要重新登录才能继续使用这些 OAuth 模型。', logout: '退出登录', cancel: '取消', - loggedOut: '已退出登录', credentialsCleared: '本地凭据已清除。', logoutFailed: '退出失败', logoutFailedRetry: '退出登录失败,请稍后重试。', + refreshFailed: '刷新登录状态失败', + accountActionFailed: (name: string) => `${name} 账号操作失败`, + loginFailedRetry: '登录失败,请稍后重试。', + retry: '请稍后再试。', + startFailed: '无法开始登录', + startFailedRetry: '无法开始登录,请稍后再试。', + openFailed: '无法打开浏览器', + openFailedRetry: '无法打开浏览器,请稍后重试。', + loginSuccess: '登录成功', + bound: (name: string) => `${name} 已绑定本机。`, + incomplete: '登录未完成', + incompleteRetry: '登录未完成,请重新打开浏览器授权。', + loginFailed: '登录失败', + logoutDescription: '将删除本机保存的订阅凭据,之后需要重新登录才能继续使用这些 OAuth 模型。', + logout: '退出登录', + cancel: '取消', + loggedOut: '已退出登录', + credentialsCleared: '本地凭据已清除。', + logoutFailed: '退出失败', + logoutFailedRetry: '退出登录失败,请稍后重试。', + reverifyFailedRetry: '重新验证失败,请稍后重试。', serviceUnavailable: '登录服务暂时不可用,请检查网络后重试。', - logoutTitle: (name: string) => `退出 ${name} 登录?`, + logoutTitle: (name: string) => `退出 ${name} 登录?` }, oauthSection: { - signedIn: '已登录', codexDescription: '使用 ChatGPT Plus / Pro 账号添加连接。', xaiDescription: '使用 SuperGrok / X Premium 账号添加连接。', + signedIn: '已登录', + codexDescription: '使用 ChatGPT Plus / Pro 账号添加连接。', + xaiDescription: '使用 SuperGrok / X Premium 账号添加连接。', configuredConnections: (count: number) => `已有 ${count} 个连接 · 添加另一个账号`, - copilotDescription: '使用 GitHub 登录连接 Copilot 订阅,或导入兼容凭据。', serviceUnavailable: '登录服务暂时不可用,请检查网络后重试。', + copilotDescription: '使用 GitHub 登录连接 Copilot 订阅,或导入兼容凭据。', + serviceUnavailable: '登录服务暂时不可用,请检查网络后重试。', aria: 'OAuth 登录', staleState: 'OAuth 登录状态暂时没刷新成功,已保留上一次状态。', - codexDetail: '点击下方按钮打开设备授权页,并在页面中输入这里显示的登录码。', xaiDetail: '点击下方按钮打开浏览器登录,授权完成后会自动回写。', deviceCode: '登录码:', - openingBrowser: '打开浏览器…', waitingAuthorization: '等待浏览器授权…', logout: '退出登录', loggingOut: '退出中…', - copilotSubtitle: '使用 GitHub 登录,或导入兼容凭据;token 不会暴露给渲染进程。', copilotImported: '已导入 GitHub Copilot 订阅账号。', - copilotSetup: '使用 GitHub 登录以连接 Copilot,或导入已有的 gh 凭据。', importing: '导入中…', - copilotSignIn: '使用 GitHub 登录', copilotActionFailed: 'GitHub Copilot 账号操作失败', + codexDetail: '点击下方按钮打开设备授权页,并在页面中输入这里显示的登录码。', + xaiDetail: '点击下方按钮打开浏览器登录,授权完成后会自动回写。', + deviceCode: '登录码:', + openingBrowser: '打开浏览器…', + waitingAuthorization: '等待浏览器授权…', + logout: '退出登录', + loggingOut: '退出中…', + copilotSubtitle: '使用 GitHub 登录,或导入兼容凭据;token 不会暴露给渲染进程。', + copilotImported: '已导入 GitHub Copilot 订阅账号。', + copilotSetup: '使用 GitHub 登录以连接 Copilot,或导入已有的 gh 凭据。', + importing: '导入中…', + copilotSignIn: '使用 GitHub 登录', + copilotActionFailed: 'GitHub Copilot 账号操作失败', copilotSignInDisabledHint: '本机未启用 GitHub 登录;请改用导入兼容凭据,或由管理员启用后重试。', + reimport: '重新导入', importCredential: '导入兼容凭据', - loadingAccount: '正在加载账号状态…', authorizing: '请在弹出的浏览器窗口完成登录。', refreshing: '正在刷新访问令牌…', refreshTokenFailed: '令牌刷新失败,请重新登录。', + verifying: '验证中…', + reverify: '重新验证', + removing: '移除中…', + removeLocal: '移除本地登录', + loadingAccount: '正在加载账号状态…', + authorizing: '请在弹出的浏览器窗口完成登录。', + refreshing: '正在刷新访问令牌…', + refreshTokenFailed: '令牌刷新失败,请重新登录。', cardAria: (intent: 'add' | 'import' | 'manage', name: string, status: string | undefined, description: string) => `${intent === 'add' ? '添加' : intent === 'import' ? '导入' : '管理'}账号连接:${name}${status ? `,状态:${status}` : ''},${description.replace(/[。.!!??]+$/u, '')}`, - connectTitle: (name: string) => `连接 ${name}`, addAccountTitle: (name: string) => `添加 ${name} 账号`, login: (name: string) => `登录 ${name}`, loginAndAdd: '登录并添加', signedOut: (name: string) => `${name} 尚未登录。`, - storageFailed: (name: string) => `${name} 本地凭据读取失败,请重新登录。`, providerUnavailable: (name: string) => `${name} 已登录,但当前 provider 状态不可用。`, - }, + connectTitle: (name: string) => `连接 ${name}`, + addAccountTitle: (name: string) => `添加 ${name} 账号`, + login: (name: string) => `登录 ${name}`, + loginAndAdd: '登录并添加', + signedOut: (name: string) => `${name} 尚未登录。`, + storageFailed: (name: string) => `${name} 本地凭据读取失败,请重新登录。`, + providerUnavailable: (name: string) => `${name} 已登录,但当前 provider 状态不可用。` + } } as const; - export type ProviderSettingsCopy = WidenCopy; - const enCopy: ProviderSettingsCopy = { detail: { - delete: 'Delete', cancel: 'Cancel', deleteUnused: 'Delete unused connection', - deleteFailed: 'Failed to delete model connection', credentialReadFailed: 'Failed to read model credential status', refreshFailed: 'Failed to refresh model connection', - saveFailed: 'Failed to save model connection', saveModelsFailed: 'Failed to save enabled models', - modelKey: 'Model key', pasteModelKey: 'Paste model key', getModelKey: 'Get model key', saving: 'Saving…', - advancedRequest: 'Advanced request settings', advancedRequestHelp: 'Add HTTP headers and extra JSON request-body fields for this connection. Header values stay in the local credential vault.', - requestHeaders: 'Custom request headers', headerName: 'Header name', headerValue: 'Header value', retainedHeaderValue: 'Keep saved value', addHeader: 'Add header', removeHeader: 'Remove', noRequestHeaders: 'No custom request headers.', - extraRequestBody: 'Extra request body (JSON)', extraRequestBodyHelp: 'Adds top-level fields only. A collision with a Maka-generated field fails explicitly.', requestCustomizationInvalid: 'Invalid request settings', requestHeadersInvalidDetail: 'Check the request header names and values.', requestBodyInvalidDetail: 'Enter a valid JSON object that meets the requirements.', saveAdvancedRequest: 'Save advanced request settings', configuredHeaders: (count: number) => `${count} ${count === 1 ? 'header' : 'headers'}`, noAdvancedRequest: 'Not configured', - oauthLoggedIn: 'OAuth signed in', oauthLoading: 'Reading OAuth status', oauthUnknown: 'OAuth status unavailable', oauthWaiting: 'Waiting for OAuth sign-in', + delete: 'Delete', + cancel: 'Cancel', + deleteUnused: 'Delete unused connection', + deleteFailed: 'Failed to delete model connection', + credentialReadFailed: 'Failed to read model credential status', + refreshFailed: 'Failed to refresh model connection', + saveFailed: 'Failed to save model connection', + saveModelsFailed: 'Failed to save enabled models', + modelKey: 'Model key', + pasteModelKey: 'Paste model key', + getModelKey: 'Get model key', + saving: 'Saving…', + advancedRequest: 'Advanced request settings', + advancedRequestHelp: 'Add HTTP headers and extra JSON request-body fields for this connection. Header values stay in the local credential vault.', + requestHeaders: 'Custom request headers', + headerName: 'Header name', + headerValue: 'Header value', + retainedHeaderValue: 'Keep saved value', + addHeader: 'Add header', + removeHeader: 'Remove', + noRequestHeaders: 'No custom request headers.', + extraRequestBody: 'Extra request body (JSON)', + extraRequestBodyHelp: 'Adds top-level fields only. A collision with a Maka-generated field fails explicitly.', + requestCustomizationInvalid: 'Invalid request settings', + requestHeadersInvalidDetail: 'Check the request header names and values.', + requestBodyInvalidDetail: 'Enter a valid JSON object that meets the requirements.', + saveAdvancedRequest: 'Save advanced request settings', + configuredHeaders: (count: number) => `${count} ${count === 1 ? 'header' : 'headers'}`, + noAdvancedRequest: 'Not configured', + oauthLoggedIn: 'OAuth signed in', + oauthLoading: 'Reading OAuth status', + oauthUnknown: 'OAuth status unavailable', + oauthWaiting: 'Waiting for OAuth sign-in', oauthLoggedInDetail: 'This model connection uses an OAuth access token stored by the main process. If a request asks you to sign in again, reauthorize it under account connections.', oauthLoadingDetail: 'Reading the local OAuth status. An unknown state will not be shown as signed out.', oauthUnknownDetail: 'The local OAuth status is temporarily unavailable. Refresh the page or reopen Settings.', oauthWaitingDetail: 'Complete sign-in under account connections. The model connection appears automatically afterward.', oauthRetired: 'This sign-in path is retired', - oauthRetiredDetail: - 'The sign-in this connection uses was removed from Maka. It can no longer be signed into or used in a conversation. Add an Anthropic API key connection to keep using Claude models; deleting this connection also clears the sign-in credential stored on this machine.', + oauthRetiredDetail: 'The sign-in this connection uses was removed from Maka. It can no longer be signed into or used in a conversation. Add an Anthropic API key connection to keep using Claude models; deleting this connection also clears the sign-in credential stored on this machine.', credentialLoadingDetail: 'Reading model credential status. Connection tests and model refresh are paused until it finishes.', credentialUnknownDetail: 'Model credential status could not be refreshed, so the connection is not being mislabeled as signed out or unconfigured.', testConnection: 'Test connection', - updateModels: 'Update model catalog', endpoint: 'Service URL', + updateModels: 'Update model catalog', + endpoint: 'Service URL', connectionName: 'Name', connectionNamePlaceholder: 'Name this connection', addModel: 'Add model', @@ -270,135 +468,667 @@ const enCopy: ProviderSettingsCopy = { addModelIdRequired: 'Enter a model ID.', addModelIdDuplicate: 'This model is already in the list.', addModelContextWindow: 'Context window', - addModelContextWindowHelp: - "The maximum token count from the provider's model page. Without it Maka can only assume 32k, and long conversations get truncated early.", + addModelContextWindowHelp: "The maximum token count from the provider's model page. Without it Maka can only assume 32k, and long conversations get truncated early.", addModelContextWindowRequired: 'Enter a context window.', - credentials: 'Connection', dangerZone: 'Delete connection', deleteRowHelp: 'This cannot be undone.', + credentials: 'Connection', + dangerZone: 'Delete connection', + deleteRowHelp: 'This cannot be undone.', credentialsHelp: 'The key stays on this machine.', credentialsHelpAccount: 'The sign-in token stays on this machine.', modelManagementHelp: 'These models appear in the chat model picker.', ...enCapabilitiesCopy, capabilitiesHelp: 'Declares thinking levels, vision, and context window per enabled model; applies on save.', - change: 'Change', set: 'Set', edit: 'Edit', save: 'Save', + change: 'Change', + set: 'Set', + edit: 'Edit', + save: 'Save', endpointManaged: 'Managed by account sign-in or the provider', endpointMissing: 'No service URL configured', endpointModelOverridesNote: 'Some models send requests through a different endpoint', endpointCredentialsMasked: 'The saved URL embeds credentials and stays masked while editing', modelManagement: 'Models', disconnectAndDelete: 'Sign out and delete', - login: 'Sign in', loggingIn: 'Signing in…', relogin: 'Sign in again', - oauthReloginDetail: 'If a request asks you to sign in again, restart authorization here.', deviceCode: 'Sign-in code:', + copilotImportFailed: 'Failed to import GitHub Copilot sign-in', + copilotLoggedIn: 'GitHub Copilot signed in', + copilotWaiting: 'Waiting for compatible GitHub credentials', + copilotLoggedInDetail: 'Reimport compatible credentials if the account or organization policy changes.', + copilotWaitingDetail: 'Configure credentials with Copilot Requests permission, then import them securely from this device.', + reimport: 'Reimport', + importCredential: 'Import compatible credentials', + login: 'Sign in', + loggingIn: 'Signing in…', + relogin: 'Sign in again', + oauthReloginDetail: 'If a request asks you to sign in again, restart authorization here.', oauthStartDetail: 'Open the browser below to sign in. This status refreshes automatically after authorization.', enabledModels: 'Enabled models', - searchModels: 'Search models', selectAllModels: 'Enable all', + searchModels: 'Search models', + selectAllModels: 'Enable all', noModels: 'No models are available. Update the model catalog first.', - keySet: 'Set', statusLoading: 'Reading status', credentialUnknown: 'Credential status unavailable', keyMissing: 'No key set', - keyTroubleshooting: 'model key, service URL, and proxy settings', endpointTroubleshooting: 'local service, service URL, and proxy settings', oauthTroubleshooting: 'OAuth sign-in and proxy settings', + keySet: 'Set', + statusLoading: 'Reading status', + credentialUnknown: 'Credential status unavailable', + keyMissing: 'No key set', + keyTroubleshooting: 'model key, service URL, and proxy settings', + endpointTroubleshooting: 'local service, service URL, and proxy settings', + oauthTroubleshooting: 'OAuth sign-in and proxy settings', unknownDescription: (provider: string) => `The provider “${provider}” used by this connection is not registered in the current version. Its configuration and credentials are preserved for a supported version.`, deleteProviderTitle: (name: string) => `Delete provider ${name}?`, deleteConnectionTitle: (name: string) => `Delete model connection ${name}?`, deleteUnknownDescription: 'After deletion, another version that supports this provider cannot restore the connection or its credentials.', - deleteDescription: (isDefault: boolean, disconnectsAccount: boolean) => [ - disconnectsAccount - ? 'This signs out the local account and deletes its OAuth credential and model connection. It will not be recreated on refresh.' - : 'This deletes the model connection and its local credential. Add it again to use it later.', - isDefault - ? 'It is currently the default connection; the default model becomes unset and existing chats may need another model selected.' - : '', - ].filter(Boolean).join(' '), - connectionSuccess: (name: string) => `Connected · ${name}`, connectionFailed: (name: string) => `Connection failed · ${name}`, + deleteDescription: (isDefault: boolean, disconnectsAccount: boolean) => [disconnectsAccount ? 'This signs out the local account and deletes its OAuth credential and model connection. It will not be recreated on refresh.' : 'This deletes the model connection and its local credential. Add it again to use it later.', isDefault ? 'It is currently the default connection; the default model becomes unset and existing chats may need another model selected.' : ''].filter(Boolean).join(' '), + connectionSuccess: (name: string) => `Connected · ${name}`, + connectionFailed: (name: string) => `Connection failed · ${name}`, connectionFallbackTitle: (name: string) => `Connection works · ${name}`, - connectionFallbackDetail: (selected: readonly string[], tested: string) => - `Your selected ${selected.join(', ')} didn't respond; verified the connection with ${tested} instead. Tasks using your selected model may fail.`, - connectionTestError: (name: string) => `Connection test error · ${name}`, modelsFetched: (count: number, name: string) => `Fetched ${count} ${count === 1 ? 'model' : 'models'} · ${name}`, + connectionFallbackDetail: (selected: readonly string[], tested: string) => `Your selected ${selected.join(', ')} didn't respond; verified the connection with ${tested} instead. Tasks using your selected model may fail.`, + connectionTestError: (name: string) => `Connection test error · ${name}`, + modelsFetched: (count: number, name: string) => `Fetched ${count} ${count === 1 ? 'model' : 'models'} · ${name}`, modelsFetchFailed: (name: string) => `Failed to fetch models · ${name}`, modelsFetchFailedDetail: (message: string, troubleshooting: string) => `${message} · The static list remains visible. Check ${troubleshooting} and try again.`, - authTroubleshooting: (value: string) => `Authentication failed. Check ${value} and try again.`, recheckTroubleshooting: (value: string) => `Check ${value} and try again.`, - modelKeyAria: (name: string) => `${name} model key`, + authTroubleshooting: (value: string) => `Authentication failed. Check ${value} and try again.`, + recheckTroubleshooting: (value: string) => `Check ${value} and try again.`, + modelKeyAria: (name: string) => `${name} model key` }, shared: { - actionFallback: 'The model connection service is temporarily unavailable. Try again later.', rateLimit: 'This account or model service is rate-limited. Try again later.', - timeout: 'The request timed out. Check the network or proxy and try again.', unavailable: 'The model service is temporarily unavailable. Try again later.', - network: 'Network error. Check the service URL or proxy settings and try again.', statusUnavailable: 'The connection test status is temporarily unavailable. Test again.', - categories: { oauth: 'OAuth', domestic: 'China', overseas: 'Global', local: 'Local', custom: 'Custom' }, - connectionStatuses: { retired: 'Retired · delete it', reauth: 'Sign-in required', disabledFailed: 'Unavailable · last connection failed', disabled: 'Unavailable', failed: 'Last connection failed' }, - lastTest: { - '连接已验证': 'Connection verified', '鉴权失败': 'Authentication failed', '请求超时': 'Request timed out', '网络错误': 'Network error', '模型服务返回错误': 'Model service returned an error', '连接测试失败': 'Connection test failed', - 'connection verified': 'Connection verified', 'authentication failed': 'Authentication failed', 'request timed out': 'Request timed out', 'network error': 'Network error', 'provider returned an error': 'Model service returned an error', 'connection test failed': 'Connection test failed', - 'claude oauth 未登录。': 'Claude OAuth is signed out.', 'claude oauth 本地凭据读取失败。': 'Could not read local Claude OAuth credentials.', 'claude oauth 需要重新登录。': 'Claude OAuth requires sign-in.', 'claude oauth 已登录。': 'Claude OAuth is signed in.', 'claude oauth 已退出登录。': 'Claude OAuth signed out.', - 'codex oauth 未登录。': 'Codex OAuth is signed out.', 'codex oauth 本地凭据读取失败。': 'Could not read local Codex OAuth credentials.', 'codex oauth 需要重新登录。': 'Codex OAuth requires sign-in.', 'codex oauth 已登录。': 'Codex OAuth is signed in.', 'codex oauth 已退出登录。': 'Codex OAuth signed out.', - '当前账号无可用 codex 模型。': 'No Codex models are available for this account.', 'codex 模型列表获取失败。': 'Failed to fetch the Codex model list.', - 'github copilot 需要重新导入 github cli 登录。': 'GitHub Copilot requires the GitHub CLI sign-in to be imported again.', 'github copilot 无法读取当前账号可用模型,请重新验证登录。': 'GitHub Copilot could not read models available to this account. Verify sign-in again.', 'github copilot 登录已导入。': 'GitHub Copilot sign-in imported.', 'github copilot 连接未能保存,请重新导入登录。': 'The GitHub Copilot connection could not be saved. Import sign-in again.', 'github copilot 已移除本地登录。': 'Local GitHub Copilot sign-in removed.', + actionFallback: 'The model connection service is temporarily unavailable. Try again later.', + rateLimit: 'This account or model service is rate-limited. Try again later.', + timeout: 'The request timed out. Check the network or proxy and try again.', + unavailable: 'The model service is temporarily unavailable. Try again later.', + network: 'Network error. Check the service URL or proxy settings and try again.', + statusUnavailable: 'The connection test status is temporarily unavailable. Test again.', + categories: { + oauth: 'OAuth', + domestic: 'China', + overseas: 'Global', + local: 'Local', + custom: 'Custom' }, + connectionStatuses: { + retired: 'Retired · delete it', + reauth: 'Sign-in required', + disabledFailed: 'Unavailable · last connection failed', + disabled: 'Unavailable', + failed: 'Last connection failed' + }, + lastTest: { + '连接已验证': 'Connection verified', + '鉴权失败': 'Authentication failed', + '请求超时': 'Request timed out', + '网络错误': 'Network error', + '模型服务返回错误': 'Model service returned an error', + '连接测试失败': 'Connection test failed', + 'connection verified': 'Connection verified', + 'authentication failed': 'Authentication failed', + 'request timed out': 'Request timed out', + 'network error': 'Network error', + 'provider returned an error': 'Model service returned an error', + 'connection test failed': 'Connection test failed', + 'claude oauth 未登录。': 'Claude OAuth is signed out.', + 'claude oauth 本地凭据读取失败。': 'Could not read local Claude OAuth credentials.', + 'claude oauth 需要重新登录。': 'Claude OAuth requires sign-in.', + 'claude oauth 已登录。': 'Claude OAuth is signed in.', + 'claude oauth 已退出登录。': 'Claude OAuth signed out.', + 'codex oauth 未登录。': 'Codex OAuth is signed out.', + 'codex oauth 本地凭据读取失败。': 'Could not read local Codex OAuth credentials.', + 'codex oauth 需要重新登录。': 'Codex OAuth requires sign-in.', + 'codex oauth 已登录。': 'Codex OAuth is signed in.', + 'codex oauth 已退出登录。': 'Codex OAuth signed out.', + '当前账号无可用 codex 模型。': 'No Codex models are available for this account.', + 'codex 模型列表获取失败。': 'Failed to fetch the Codex model list.', + 'github copilot 需要重新导入 github cli 登录。': 'GitHub Copilot requires the GitHub CLI sign-in to be imported again.', + 'github copilot 无法读取当前账号可用模型,请重新验证登录。': 'GitHub Copilot could not read models available to this account. Verify sign-in again.', + 'github copilot 登录已导入。': 'GitHub Copilot sign-in imported.', + 'github copilot 连接未能保存,请重新导入登录。': 'The GitHub Copilot connection could not be saved. Import sign-in again.', + 'github copilot 已移除本地登录。': 'Local GitHub Copilot sign-in removed.' + } }, panel: { - tabs: { all: 'All', recommended: 'Recommended', accounts: 'Accounts', plans: 'Model plans', api: 'API', aggregators: 'Aggregators', local: 'Local' }, - loadFailed: 'Failed to load model connections', loadingAria: 'Loading model providers', connections: 'Connections', - retry: 'Select to retry.', empty: 'No model connections yet', connectionRemoved: 'The original connection was deleted or removed. Returned to the connection list.', - connectedLoading: 'Connection added. Loading its details…', connectedLoadFailed: 'Connection added, but the connection list could not be refreshed yet.', + tabs: { + all: 'All', + recommended: 'Recommended', + accounts: 'Accounts', + plans: 'Model plans', + api: 'API', + aggregators: 'Aggregators', + local: 'Local' + }, + loadFailed: 'Failed to load model connections', + loadingAria: 'Loading model providers', + connections: 'Connections', + retry: 'Select to retry.', + empty: 'No model connections yet', + connectionRemoved: 'The original connection was deleted or removed. Returned to the connection list.', + connectedLoading: 'Connection added. Loading its details…', + connectedLoadFailed: 'Connection added, but the connection list could not be refreshed yet.', connectionIdentityChanged: 'The new connection identity did not match the sign-in result. Return to the connection list and try again.', - emptyHelp: 'Choose a connection method below to begin.', default: 'Default', setDefault: 'Set as default', setDefaultTitle: 'New chats will use this connection', setDefaultPending: 'Setting…', setDefaultFailed: 'Could not set as default', addHelp: 'Choose account sign-in, a model plan, API, aggregator, or local runtime.', - categoriesAria: 'Model provider categories', searchPlaceholder: 'Search providers', searchAria: 'Search model providers', noMatch: 'No matching providers', clearSearch: 'Clear search', - createSubtitle: 'After required setup, the connection appears above on the Models page.', connection: 'Model connection', - count: (value: number) => `· ${value}`, connectTitle: (name: string) => `Connect ${name}`, + emptyHelp: 'Choose a connection method below to begin.', + default: 'Default', + setDefault: 'Set as default', + setDefaultTitle: 'New chats will use this connection', + setDefaultPending: 'Setting…', + setDefaultFailed: 'Could not set as default', + addHelp: 'Choose account sign-in, a model plan, API, aggregator, or local runtime.', + categoriesAria: 'Model provider categories', + searchPlaceholder: 'Search providers', + searchAria: 'Search model providers', + noMatch: 'No matching providers', + clearSearch: 'Clear search', + createSubtitle: 'After required setup, the connection appears above on the Models page.', + connection: 'Model connection', + count: (value: number) => `· ${value}`, + connectTitle: (name: string) => `Connect ${name}`, chipAria: (name: string, provider: string, isDefault: boolean, status?: string) => `Model connection: ${name}; provider: ${provider}${isDefault ? '; default connection' : ''}${status ? `; ${status}` : ''}`, - addConnection: 'Add connection', category: 'Category', backToList: 'Back to model connections', backToCatalog: 'Back to the provider list', + addConnection: 'Add connection', + category: 'Category', + backToList: 'Back to model connections', + backToCatalog: 'Back to the provider list' }, catalog: { unavailable: 'Unavailable', - cardAria: (name: string, description: string) => `Add model provider: ${name}; ${description}`, + cardAria: (name: string, description: string) => `Add model provider: ${name}; ${description}` }, add: { - invalidSlug: 'The connection identifier format is invalid', duplicateSlug: 'Connection identifier already exists', cloudflareAccount: 'Enter the Cloudflare Account ID', endpointRequired: 'This provider requires a service URL', + invalidSlug: 'The connection identifier format is invalid', + duplicateSlug: 'Connection identifier already exists', + cloudflareAccount: 'Enter the Cloudflare Account ID', + endpointRequired: 'This provider requires a service URL', accountLogin: 'Complete sign-in under account connections. A model connection is created automatically afterward.', - apiKeyPlaceholder: 'Enter or paste API key', cancel: 'Cancel', accountTitle: 'Sign in with an account connection', - advancedRequest: 'Advanced request settings', expandAdvancedRequest: 'Show advanced request settings', collapseAdvancedRequest: 'Hide advanced request settings', - requestHeaders: 'Custom request headers', headerName: 'Header name', headerValue: 'Header value', retainedHeaderValue: 'Keep saved value', addHeader: 'Add header', removeHeader: 'Remove', noRequestHeaders: 'No custom request headers.', - extraRequestBody: 'Extra request body (JSON)', extraRequestBodyHelp: 'Adds top-level fields only. A collision with a Maka-generated field fails explicitly. Header values stay in the local credential vault.', requestCustomizationInvalid: 'Check the advanced request settings.', + apiKeyPlaceholder: 'Enter or paste API key', + cancel: 'Cancel', + accountTitle: 'Sign in with an account connection', + advancedRequest: 'Advanced request settings', + expandAdvancedRequest: 'Show advanced request settings', + collapseAdvancedRequest: 'Hide advanced request settings', + requestHeaders: 'Custom request headers', + headerName: 'Header name', + headerValue: 'Header value', + retainedHeaderValue: 'Keep saved value', + addHeader: 'Add header', + removeHeader: 'Remove', + noRequestHeaders: 'No custom request headers.', + extraRequestBody: 'Extra request body (JSON)', + extraRequestBodyHelp: 'Adds top-level fields only. A collision with a Maka-generated field fails explicitly. Header values stay in the local credential vault.', + requestCustomizationInvalid: 'Check the advanced request settings.', accountDetail: 'Do not add this provider manually here. Return to account connections and sign in; Maka creates and refreshes the model connection automatically.', - slug: 'Connection identifier', name: 'Display name', + slug: 'Connection identifier', + name: 'Display name', accountIdPlaceholder: 'Enter account ID', - saving: 'Saving…', save: 'Save provider', keyRequired: (name: string) => `Enter the ${name} API key`, - apiKeyLabel: 'API key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: 'Service URL', - defaultModel: 'Default model', defaultModelPlaceholder: 'Leave empty — fetched after saving', defaultModelHelp: 'Maka fetches the model catalog from this endpoint after saving. Type a model id here only if the endpoint serves no catalog.', - onboardingVerifyAndChoose: 'Verify and choose models', onboardingVerifying: 'Verifying the key and loading models…', - onboardingChooseModels: 'Choose models for this connection', onboardingChooseModelsHelp: 'You can enable more models from the connection details later.', onboardingEnabledModels: 'Enabled models', onboardingSearchModels: 'Search models', onboardingAddConnection: 'Add connection', onboardingBack: 'Back to edit', - onboardingAuthFailed: 'The key could not be verified. Check it and try again.', onboardingTimeout: 'Verification timed out. Check the network or proxy and try again.', onboardingNetwork: 'Could not reach the model provider. Check the network, proxy, or service URL.', onboardingUnavailable: 'The model connection service is unavailable. Try again later.', onboardingInvalidResponse: 'The model provider returned an unrecognized response. Try again later.', onboardingCatalogFull: 'The connection limit has been reached. Remove an unused connection first.', onboardingModelsChanged: 'The available models changed. Verify again and make a new selection.', onboardingNoModels: 'No usable models were found. No connection was created.', onboardingSelectModel: 'Enable at least one model.', - onboardingOutcomeUnknown: 'The save result cannot be confirmed', onboardingOutcomeUnknownDetail: 'Do not add it again yet, because that could create a duplicate connection. Reload the connection list and check whether this connection appeared; reconnect the Runtime Host if the result is still unclear.', onboardingReloadConnections: 'Reload connection list', onboardingRestart: 'Add another connection anyway', - ...enCapabilitiesCopy, + saving: 'Saving…', + save: 'Save provider', + keyRequired: (name: string) => `Enter the ${name} API key`, + apiKeyLabel: 'API key', + accountIdLabel: 'Cloudflare Account ID', + endpointLabel: 'Service URL', + defaultModel: 'Default model', + defaultModelPlaceholder: 'Leave empty — fetched after saving', + defaultModelHelp: 'Maka fetches the model catalog from this endpoint after saving. Type a model id here only if the endpoint serves no catalog.', + onboardingVerifyAndChoose: 'Verify and choose models', + onboardingVerifying: 'Verifying the key and loading models…', + onboardingChooseModels: 'Choose models for this connection', + onboardingChooseModelsHelp: 'You can enable more models from the connection details later.', + onboardingEnabledModels: 'Enabled models', + onboardingSearchModels: 'Search models', + onboardingAddConnection: 'Add connection', + onboardingBack: 'Back to edit', + onboardingAuthFailed: 'The key could not be verified. Check it and try again.', + onboardingTimeout: 'Verification timed out. Check the network or proxy and try again.', + onboardingNetwork: 'Could not reach the model provider. Check the network, proxy, or service URL.', + onboardingUnavailable: 'The model connection service is unavailable. Try again later.', + onboardingInvalidResponse: 'The model provider returned an unrecognized response. Try again later.', + onboardingCatalogFull: 'The connection limit has been reached. Remove an unused connection first.', + onboardingModelsChanged: 'The available models changed. Verify again and make a new selection.', + onboardingNoModels: 'No usable models were found. No connection was created.', + onboardingSelectModel: 'Enable at least one model.', + onboardingOutcomeUnknown: 'The save result cannot be confirmed', + onboardingOutcomeUnknownDetail: 'Do not add it again yet, because that could create a duplicate connection. Reload the connection list and check whether this connection appeared; reconnect the Runtime Host if the result is still unclear.', + onboardingReloadConnections: 'Reload connection list', + onboardingRestart: 'Add another connection anyway', + ...enCapabilitiesCopy }, oauthFlow: { - refreshFailed: 'Failed to refresh sign-in status', accountActionFailed: (name: string) => `${name} account action failed`, loginFailedRetry: 'Sign-in failed. Try again later.', - retry: 'Try again later.', startFailed: 'Could not start sign-in', startFailedRetry: 'Could not start sign-in. Try again later.', openFailed: 'Could not open browser', - openFailedRetry: 'Could not open the browser. Try again.', loginSuccess: 'Signed in', bound: (name: string) => `${name} is connected to this device.`, - incomplete: 'Sign-in incomplete', incompleteRetry: 'Sign-in did not finish. Reopen the browser authorization and try again.', loginFailed: 'Sign-in failed', - logoutDescription: 'This removes the locally stored subscription credentials. You must sign in again to use these OAuth models.', logout: 'Sign out', cancel: 'Cancel', - loggedOut: 'Signed out', credentialsCleared: 'Local credentials cleared.', logoutFailed: 'Sign-out failed', logoutFailedRetry: 'Sign-out failed. Try again later.', + refreshFailed: 'Failed to refresh sign-in status', + accountActionFailed: (name: string) => `${name} account action failed`, + loginFailedRetry: 'Sign-in failed. Try again later.', + retry: 'Try again later.', + startFailed: 'Could not start sign-in', + startFailedRetry: 'Could not start sign-in. Try again later.', + openFailed: 'Could not open browser', + openFailedRetry: 'Could not open the browser. Try again.', + loginSuccess: 'Signed in', + bound: (name: string) => `${name} is connected to this device.`, + incomplete: 'Sign-in incomplete', + incompleteRetry: 'Sign-in did not finish. Reopen the browser authorization and try again.', + loginFailed: 'Sign-in failed', + logoutDescription: 'This removes the locally stored subscription credentials. You must sign in again to use these OAuth models.', + logout: 'Sign out', + cancel: 'Cancel', + loggedOut: 'Signed out', + credentialsCleared: 'Local credentials cleared.', + logoutFailed: 'Sign-out failed', + logoutFailedRetry: 'Sign-out failed. Try again later.', + reverifyFailedRetry: 'Verification failed. Try again later.', serviceUnavailable: 'The sign-in service is temporarily unavailable. Check the network and try again.', - logoutTitle: (name: string) => `Sign out of ${name}?`, + logoutTitle: (name: string) => `Sign out of ${name}?` }, oauthSection: { - signedIn: 'Signed in', codexDescription: 'Use a ChatGPT Plus / Pro account to add a connection.', xaiDescription: 'Use a SuperGrok or X Premium account to add a connection.', + signedIn: 'Signed in', + codexDescription: 'Use a ChatGPT Plus / Pro account to add a connection.', + xaiDescription: 'Use a SuperGrok or X Premium account to add a connection.', configuredConnections: (count: number) => `${count} ${count === 1 ? 'connection' : 'connections'} configured · Add another account`, - copilotDescription: 'Sign in with GitHub to connect a Copilot subscription, or import compatible credentials.', serviceUnavailable: 'The sign-in service is temporarily unavailable. Check the network and try again.', + copilotDescription: 'Sign in with GitHub to connect a Copilot subscription, or import compatible credentials.', + serviceUnavailable: 'The sign-in service is temporarily unavailable. Check the network and try again.', aria: 'OAuth sign-in', staleState: 'OAuth sign-in status could not be refreshed. The last known state is preserved. ', - codexDetail: 'Open the device page below and enter the sign-in code shown here.', xaiDetail: 'Open the browser below to sign in. Authorization is written back automatically.', deviceCode: 'Sign-in code:', - openingBrowser: 'Opening browser…', waitingAuthorization: 'Waiting for browser authorization…', logout: 'Sign out', loggingOut: 'Signing out…', - copilotSubtitle: 'Sign in with GitHub, or import compatible credentials. The token is never exposed to the renderer.', copilotImported: 'GitHub Copilot subscription account imported.', - copilotSetup: 'Sign in with GitHub to connect Copilot, or import an existing gh credential.', importing: 'Importing…', - copilotSignIn: 'Sign in with GitHub', copilotActionFailed: 'GitHub Copilot account action failed', + codexDetail: 'Open the device page below and enter the sign-in code shown here.', + xaiDetail: 'Open the browser below to sign in. Authorization is written back automatically.', + deviceCode: 'Sign-in code:', + openingBrowser: 'Opening browser…', + waitingAuthorization: 'Waiting for browser authorization…', + logout: 'Sign out', + loggingOut: 'Signing out…', + copilotSubtitle: 'Sign in with GitHub, or import compatible credentials. The token is never exposed to the renderer.', + copilotImported: 'GitHub Copilot subscription account imported.', + copilotSetup: 'Sign in with GitHub to connect Copilot, or import an existing gh credential.', + importing: 'Importing…', + copilotSignIn: 'Sign in with GitHub', + copilotActionFailed: 'GitHub Copilot account action failed', copilotSignInDisabledHint: 'GitHub sign-in is not enabled on this install. Import a compatible credential instead, or ask an operator to enable it.', + reimport: 'Reimport', importCredential: 'Import compatible credentials', - loadingAccount: 'Loading account status…', authorizing: 'Complete sign-in in the browser window.', refreshing: 'Refreshing access token…', refreshTokenFailed: 'Token refresh failed. Sign in again.', + verifying: 'Verifying…', + reverify: 'Verify again', + removing: 'Removing…', + removeLocal: 'Remove local sign-in', + loadingAccount: 'Loading account status…', + authorizing: 'Complete sign-in in the browser window.', + refreshing: 'Refreshing access token…', + refreshTokenFailed: 'Token refresh failed. Sign in again.', cardAria: (intent: 'add' | 'import' | 'manage', name: string, status: string | undefined, description: string) => `${intent === 'add' ? 'Add' : intent === 'import' ? 'Import' : 'Manage'} account connection: ${name}${status ? `; status: ${status}` : ''}; ${description.replace(/[。.!!??]+$/u, '')}`, - connectTitle: (name: string) => `Connect ${name}`, addAccountTitle: (name: string) => `Add ${name} account`, login: (name: string) => `Sign in to ${name}`, loginAndAdd: 'Sign in and add', signedOut: (name: string) => `${name} is signed out.`, - storageFailed: (name: string) => `Could not read local credentials for ${name}. Sign in again.`, providerUnavailable: (name: string) => `${name} is signed in, but the provider status is currently unavailable.`, - }, + connectTitle: (name: string) => `Connect ${name}`, + addAccountTitle: (name: string) => `Add ${name} account`, + login: (name: string) => `Sign in to ${name}`, + loginAndAdd: 'Sign in and add', + signedOut: (name: string) => `${name} is signed out.`, + storageFailed: (name: string) => `Could not read local credentials for ${name}. Sign in again.`, + providerUnavailable: (name: string) => `${name} is signed in, but the provider status is currently unavailable.` + } }; - -const PROVIDER_SETTINGS_COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog; - +const PROVIDER_SETTINGS_COPY = { + zh: zhCopy, + en: enCopy, + ko: { + detail: { + delete: "삭제", + cancel: "취소", + deleteUnused: "사용하지 않는 연결 삭제", + deleteFailed: "모델 연결을 삭제하지 못했습니다.", + credentialReadFailed: "모델 자격 증명 상태를 읽지 못했습니다.", + refreshFailed: "모델 연결을 새로 고치지 못했습니다.", + saveFailed: "모델 연결을 저장하지 못했습니다.", + saveModelsFailed: "활성화된 모델을 저장하지 못했습니다.", + modelKey: "모델 키", + pasteModelKey: "모델 키 붙여넣기", + getModelKey: "모델 키 가져오기", + saving: "절약…", + advancedRequest: "고급 요청 설정", + advancedRequestHelp: "이 연결에 대해 HTTP 헤더와 추가 JSON 요청 본문 필드를 추가하세요. 헤더 값은 로컬 자격 증명 저장소에 유지됩니다.", + requestHeaders: "사용자 정의 요청 헤더", + headerName: "헤더 이름", + headerValue: "헤더 값", + retainedHeaderValue: "저장된 값 유지", + addHeader: "헤더 추가", + removeHeader: "제거하다", + noRequestHeaders: "사용자 정의 요청 헤더가 없습니다.", + extraRequestBody: "추가 요청 본문(JSON)", + extraRequestBodyHelp: "최상위 필드만 추가합니다. Maka 생성 필드와의 충돌은 명시적으로 실패합니다.", + requestCustomizationInvalid: "잘못된 요청 설정", + requestHeadersInvalidDetail: "요청 헤더 이름과 값을 확인하세요.", + requestBodyInvalidDetail: "요구 사항을 충족하는 유효한 JSON 개체를 입력하세요.", + saveAdvancedRequest: "고급 요청 설정 저장", + configuredHeaders: (count: number) => `${count}개 헤더`, + noAdvancedRequest: "구성되지 않음", + oauthLoggedIn: "OAuth에 로그인됨", + oauthLoading: "OAuth 상태 읽기", + oauthUnknown: "OAuth 상태를 사용할 수 없습니다.", + oauthWaiting: "OAuth 로그인을 기다리는 중", + oauthLoggedInDetail: "이 모델 연결은 기본 프로세스에 저장된 OAuth 액세스 토큰을 사용합니다. 요청에서 다시 로그인하라는 메시지가 표시되면 계정 연결에서 다시 인증하세요.", + oauthLoadingDetail: "로컬 OAuth 상태를 읽습니다. 알 수 없는 상태는 로그아웃된 것으로 표시되지 않습니다.", + oauthUnknownDetail: "로컬 OAuth 상태를 일시적으로 사용할 수 없습니다. 페이지를 새로 고치거나 설정을 다시 엽니다.", + oauthWaitingDetail: "계정 연결에서 로그인을 완료하세요. 나중에 모델 연결이 자동으로 나타납니다.", + oauthRetired: "이 로그인 경로는 더 이상 사용되지 않습니다.", + oauthRetiredDetail: "이 연결에서 사용하는 로그인이 Maka에서 삭제되었습니다. 더 이상 로그인하거나 대화에서 사용할 수 없습니다. Claude 모델을 계속 사용하려면 Anthropic API 키 연결을 추가하세요. 이 연결을 삭제하면 이 컴퓨터에 저장된 로그인 자격 증명도 지워집니다.", + credentialLoadingDetail: "모델 자격 증명 상태를 읽는 중입니다. 연결 테스트 및 모델 새로 고침은 완료될 때까지 일시 중지됩니다.", + credentialUnknownDetail: "모델 자격 증명 상태를 새로 고칠 수 없으므로 연결이 로그아웃되었거나 구성되지 않은 것으로 잘못 표시되지 않습니다.", + testConnection: "테스트 연결", + updateModels: "모델 카탈로그 업데이트", + endpoint: "서비스 URL", + connectionName: "이름", + connectionNamePlaceholder: "이 연결의 이름을 지정하세요.", + addModel: "모델 추가", + addModelConfirm: "추가하다", + addModelIdField: "모델 ID", + addModelIdFieldHelp: "대소문자를 포함하여 공급자와 정확히 일치해야 합니다.", + addModelIdPlaceholder: 'deepseek-v4-pro-beta', + addModelIdRequired: "모델 ID를 입력하세요.", + addModelIdDuplicate: "이 모델은 이미 목록에 있습니다.", + addModelContextWindow: "컨텍스트 창", + addModelContextWindowHelp: "공급자 모델 페이지의 최대 토큰 수입니다. 그것이 없으면 Maka는 32k만 가정할 수 있으며 긴 대화는 일찍 잘립니다.", + addModelContextWindowRequired: "컨텍스트 창을 입력합니다.", + credentials: "연결", + dangerZone: "연결 삭제", + deleteRowHelp: "이 작업은 취소할 수 없습니다.", + credentialsHelp: "열쇠는 이 기계에 남아있습니다.", + credentialsHelpAccount: "로그인 토큰은 이 컴퓨터에 유지됩니다.", + modelManagementHelp: "이러한 모델은 채팅 모델 선택기에 표시됩니다.", + capabilities: "기능", + thinkingEffort: "사고 수준(reasoning_effort)", + thinkingEffortHelp: "이 모델이 지원하는 사고 수준을 선택하세요. 체크 표시가 없으면 선언되지 않았음을 의미합니다.", + thinkingUndeclared: "선언되지 않음", + thinkingSelectedCount: (count: number) => `${count} 선택됨`, + thinkingBulk: "모든 모델의 사고 수준 설정", + thinkingBulkHelp: "선택하면 아래의 활성화된 모든 모델에 레벨이 추가됩니다. 선택을 취소하면 모든 항목에서 해당 항목이 제거됩니다. 다른 레벨은 그대로 둡니다.", + thinkingBulkTrigger: "모든 모델에 적용", + thinkingBulkCoverage: (declared: number, total: number) => declared === 0 ? "모델 없음" : `${declared} 모델의 ${total}에서`, + visionInput: "비전 입력", + visionInputHelp: "자동은 내장된 메타데이터를 따릅니다. 활성화/비활성화는 이를 명시적으로 재정의합니다.", + visionAuto: "자동", + visionEnabledOption: "활성화됨", + visionDisabledOption: "장애가 있는", + contextWindow: "컨텍스트 창(토큰)", + contextWindowHelp: "설정되면 압축 및 예산이 이 값을 사용합니다. 비어 있으면 내장된 메타데이터가 결정합니다.", + saveCapabilities: "기능 선언 저장", + fastMode: "빠른 모드", + fastModeHelp: "OpenAI의 빠른 서비스 계층을 사용하세요. 비어 있음은 공급자 기본값을 따릅니다.", + fastAuto: "자동", + fastEnabled: "빠른", + capabilitiesHelp: "활성화된 모델별로 사고 수준, 비전 및 컨텍스트 창을 선언합니다. 저장 시 적용됩니다.", + change: "변화", + set: "세트", + edit: "편집하다", + save: "구하다", + endpointManaged: "계정 로그인 또는 공급자가 관리함", + endpointMissing: "서비스 URL이 구성되지 않았습니다.", + endpointModelOverridesNote: "일부 모델은 다른 엔드포인트를 통해 요청을 보냅니다.", + endpointCredentialsMasked: "저장된 URL은 자격 증명을 포함하고 편집하는 동안 마스크된 상태로 유지됩니다.", + modelManagement: "모델", + disconnectAndDelete: "로그아웃 및 삭제", + copilotImportFailed: "GitHub Copilot 로그인을 가져오지 못했습니다.", + copilotLoggedIn: "GitHub Copilot에 로그인됨", + copilotWaiting: "호환되는 GitHub 자격 증명을 기다리는 중", + copilotLoggedInDetail: "계정 또는 조직 정책이 변경되면 호환되는 자격 증명을 다시 가져옵니다.", + copilotWaitingDetail: "Copilot 요청 권한으로 자격 증명을 구성한 다음 이 장치에서 안전하게 가져옵니다.", + reimport: "다시 가져오기", + importCredential: "호환되는 자격 증명 가져오기", + login: "로그인", + loggingIn: "로그인 중…", + relogin: "다시 로그인", + oauthReloginDetail: "다시 로그인하라는 요청이 있으면 여기에서 승인을 다시 시작하세요.", + oauthStartDetail: "아래 브라우저를 열어 로그인하세요. 이 상태는 승인 후 자동으로 새로고침됩니다.", + enabledModels: "활성화된 모델", + searchModels: "모델 검색", + selectAllModels: "모두 활성화", + noModels: "사용할 수 있는 모델이 없습니다. 먼저 모델 카탈로그를 업데이트하세요.", + keySet: "세트", + statusLoading: "읽기 상태", + credentialUnknown: "자격 증명 상태를 알 수 없음", + keyMissing: "키가 설정되지 않았습니다.", + keyTroubleshooting: "모델 키, 서비스 URL 및 프록시 설정", + endpointTroubleshooting: "로컬 서비스, 서비스 URL 및 프록시 설정", + oauthTroubleshooting: "OAuth 로그인 및 프록시 설정", + unknownDescription: (provider: string) => `이 연결에 사용된 공급자 "${provider}"은 현재 버전에 등록되어 있지 않습니다. 해당 구성 및 자격 증명은 지원되는 버전에 대해 보존됩니다.`, + deleteProviderTitle: (name: string) => `공급자 ${name}을 삭제하시겠습니까?`, + deleteConnectionTitle: (name: string) => `모델 연결 ${name}을 삭제하시겠습니까?`, + deleteUnknownDescription: "삭제 후에는 이 공급자를 지원하는 다른 버전에서 연결이나 해당 자격 증명을 복원할 수 없습니다.", + deleteDescription: (isDefault: boolean, disconnectsAccount: boolean) => [disconnectsAccount ? "그러면 로컬 계정이 로그아웃되고 OAuth 자격 증명 및 모델 연결이 삭제됩니다. 새로 고침 시 다시 생성되지 않습니다." : "그러면 모델 연결과 해당 로컬 자격 증명이 삭제됩니다. 나중에 다시 사용하려면 다시 추가하세요.", isDefault ? "현재는 기본 연결입니다. 기본 모델이 설정 해제되고 기존 채팅에 다른 모델을 선택해야 할 수도 있습니다." : ''].filter(Boolean).join(' '), + connectionSuccess: (name: string) => `연결됨 · ${name}`, + connectionFailed: (name: string) => `연결 실패 · ${name}`, + connectionFallbackTitle: (name: string) => `연결이 작동합니다 · ${name}`, + connectionFallbackDetail: (selected: readonly string[], tested: string) => `선택한 ${selected.join(', ')}이 응답하지 않았습니다. 대신 ${tested}과의 연결을 확인했습니다. 선택한 모델을 사용하는 작업이 실패할 수 있습니다.`, + connectionTestError: (name: string) => `연결 테스트 오류 · ${name}`, + modelsFetched: (count: number, name: string) => `${count}개 모델 가져옴 · ${name}`, + modelsFetchFailed: (name: string) => `모델을 가져오지 못했습니다. · ${name}`, + modelsFetchFailedDetail: (message: string, troubleshooting: string) => `${message} · 정적 목록은 계속 표시됩니다. ${troubleshooting}을 확인하고 다시 시도하세요.`, + authTroubleshooting: (value: string) => `인증에 실패했습니다. ${value}을(를) 확인한 후 다시 시도하세요.`, + recheckTroubleshooting: (value: string) => `${value}을(를) 확인한 후 다시 시도하세요.`, + modelKeyAria: (name: string) => `${name} 모델 키` + }, + shared: { + actionFallback: '모델 연결 서비스를 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.', + rateLimit: '이 계정 또는 모델 서비스가 속도 제한에 도달했습니다. 나중에 다시 시도하세요.', + timeout: '요청 시간이 초과되었습니다. 네트워크 또는 프록시를 확인한 후 다시 시도하세요.', + unavailable: '모델 서비스를 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.', + network: '네트워크 오류입니다. 서비스 URL 또는 프록시 설정을 확인한 후 다시 시도하세요.', + statusUnavailable: '연결 테스트 상태를 일시적으로 사용할 수 없습니다. 다시 테스트하세요.', + categories: { + oauth: 'OAuth', domestic: '중국', overseas: '글로벌', local: '로컬', custom: '사용자 지정' + }, + connectionStatuses: { + retired: '사용 중지됨 · 삭제하세요', reauth: '다시 로그인해야 함', + disabledFailed: '사용 불가 · 최근 연결 실패', disabled: '사용 불가', failed: '최근 연결 실패' + }, + lastTest: { + '连接已验证': '연결 확인됨', + '鉴权失败': '인증 실패', + '请求超时': '요청 시간 초과', + '网络错误': "네트워크 오류", + '模型服务返回错误': '모델 서비스에서 오류를 반환함', + '连接测试失败': '연결 테스트 실패', + 'connection verified': '연결 확인됨', + 'authentication failed': '인증 실패', + 'request timed out': '요청 시간 초과', + 'network error': "네트워크 오류", + 'provider returned an error': '모델 서비스에서 오류를 반환함', + 'connection test failed': '연결 테스트 실패', + 'claude oauth 未登录。': 'Claude OAuth가 로그아웃되었습니다.', + 'claude oauth 本地凭据读取失败。': '로컬 Claude OAuth 자격 증명을 읽지 못했습니다.', + 'claude oauth 需要重新登录。': 'Claude OAuth에 다시 로그인해야 합니다.', + 'claude oauth 已登录。': 'Claude OAuth에 로그인되어 있습니다.', + 'claude oauth 已退出登录。': 'Claude OAuth에서 로그아웃되었습니다.', + 'codex oauth 未登录。': 'Codex OAuth가 로그아웃되었습니다.', + 'codex oauth 本地凭据读取失败。': '로컬 Codex OAuth 자격 증명을 읽지 못했습니다.', + 'codex oauth 需要重新登录。': 'Codex OAuth에 다시 로그인해야 합니다.', + 'codex oauth 已登录。': 'Codex OAuth에 로그인되어 있습니다.', + 'codex oauth 已退出登录。': 'Codex OAuth에서 로그아웃되었습니다.', + '当前账号无可用 codex 模型。': '이 계정에서 사용할 수 있는 Codex 모델이 없습니다.', + 'codex 模型列表获取失败。': 'Codex 모델 목록을 가져오지 못했습니다.', + 'github copilot 需要重新导入 github cli 登录。': 'GitHub CLI 로그인을 다시 가져와야 합니다.', + 'github copilot 无法读取当前账号可用模型,请重新验证登录。': '현재 계정에서 사용 가능한 모델을 읽지 못했습니다. 다시 로그인하세요.', + 'github copilot 登录已导入。': 'GitHub Copilot 로그인을 가져왔습니다.', + 'github copilot 连接未能保存,请重新导入登录。': 'GitHub Copilot 연결을 저장하지 못했습니다. 로그인을 다시 가져오세요.', + 'github copilot 已移除本地登录。': '로컬 GitHub Copilot 로그인이 제거되었습니다.' + } + }, + panel: { + tabs: { + all: "모두", + recommended: '추천', + accounts: '계정', + plans: '모델 플랜', + api: 'API', + aggregators: '애그리게이터', + local: '로컬' + }, + loadFailed: '모델 연결을 로드하지 못했습니다', + loadingAria: '모델 공급자 로드 중', + connections: '연결', + retry: '선택하여 다시 시도하세요.', + empty: '아직 모델 연결이 없습니다', + connectionRemoved: '원래 연결이 삭제되거나 제거되었습니다. 연결 목록으로 돌아갑니다.', + connectedLoading: '연결이 추가되었습니다. 세부 정보 로드 중…', + connectedLoadFailed: '연결은 추가되었지만 연결 목록을 아직 새로 고칠 수 없습니다.', + connectionIdentityChanged: '새 연결 정보가 로그인 결과와 일치하지 않습니다. 연결 목록으로 돌아가 다시 시도하세요.', + emptyHelp: '아래에서 연결 방법을 선택하여 시작하세요.', + default: "기본", + setDefault: "기본값으로 설정", + setDefaultTitle: '새 채팅에서 이 연결을 사용합니다', + setDefaultPending: '설정 중…', + setDefaultFailed: '기본값으로 설정하지 못했습니다', + addHelp: '계정 로그인, 모델 플랜, API, 애그리게이터 또는 로컬 런타임을 선택하세요.', + categoriesAria: '모델 공급자 카테고리', + searchPlaceholder: '공급자 검색', + searchAria: '모델 공급자 검색', + noMatch: '일치하는 공급자가 없습니다', + clearSearch: "검색 지우기", + createSubtitle: '필수 설정을 완료하면 모델 페이지 위에 연결이 표시됩니다.', + connection: "모델 연결", + count: (value: number) => `· ${value}`, + connectTitle: (name: string) => `${name} 연결`, + chipAria: (name: string, provider: string, isDefault: boolean, status?: string) => `모델 연결: ${name}; 공급자: ${provider}${isDefault ? '; 기본 연결' : ''}${status ? `; ${status}` : ''}`, + addConnection: '연결 추가', + category: '카테고리', + backToList: '모델 연결로 돌아가기', + backToCatalog: '공급자 목록으로 돌아가기' + }, + catalog: { + unavailable: '사용 불가', + cardAria: (name: string, description: string) => `모델 공급자 추가: ${name}; ${description}` + }, + add: { + invalidSlug: '연결 식별자 형식이 잘못되었습니다', + duplicateSlug: '연결 식별자가 이미 존재합니다', + cloudflareAccount: 'Cloudflare 계정 ID를 입력하세요', + endpointRequired: '이 공급자에는 서비스 URL이 필요합니다', + accountLogin: '계정 연결에서 로그인을 완료하세요. 이후 모델 연결이 자동으로 생성됩니다.', + apiKeyPlaceholder: 'API 키를 입력하거나 붙여넣으세요', + cancel: "취소", + accountTitle: '계정 연결로 로그인', + advancedRequest: "고급 요청 설정", + expandAdvancedRequest: '고급 요청 설정 표시', + collapseAdvancedRequest: '고급 요청 설정 숨기기', + requestHeaders: "사용자 정의 요청 헤더", + headerName: "헤더 이름", + headerValue: "헤더 값", + retainedHeaderValue: "저장된 값 유지", + addHeader: "헤더 추가", + removeHeader: "제거하다", + noRequestHeaders: "사용자 정의 요청 헤더가 없습니다.", + extraRequestBody: "추가 요청 본문(JSON)", + extraRequestBodyHelp: '최상위 필드만 추가합니다. Maka 생성 필드와 충돌하면 명시적으로 실패합니다. 헤더 값은 로컬 자격 증명 저장소에 보관됩니다.', + requestCustomizationInvalid: '고급 요청 설정을 확인하세요.', + accountDetail: '여기에서 이 공급자를 수동으로 추가하지 마세요. 계정 연결로 돌아가 로그인하면 Maka가 모델 연결을 자동으로 생성하고 새로 고칩니다.', + slug: '연결 식별자', + name: "표시 이름", + accountIdPlaceholder: '계정 ID 입력', + saving: "절약…", + save: '공급자 저장', + keyRequired: (name: string) => `${name} API 키를 입력하세요`, + apiKeyLabel: 'API 키', + accountIdLabel: 'Cloudflare 계정 ID', + endpointLabel: "서비스 URL", + defaultModel: '기본 모델', + defaultModelPlaceholder: '비워 두면 저장 후 가져옵니다', + defaultModelHelp: '저장 후 Maka가 이 엔드포인트에서 모델 카탈로그를 가져옵니다. 엔드포인트에 카탈로그가 없을 때만 여기에 모델 ID를 입력하세요.', + onboardingVerifyAndChoose: '모델 확인 및 선택', + onboardingVerifying: '키 확인 및 모델 로드 중…', + onboardingChooseModels: '이 연결에 사용할 모델 선택', + onboardingChooseModelsHelp: '나중에 연결 세부 정보에서 더 많은 모델을 활성화할 수 있습니다.', + onboardingEnabledModels: "활성화된 모델", + onboardingSearchModels: "모델 검색", + onboardingAddConnection: '연결 추가', + onboardingBack: '편집으로 돌아가기', + onboardingAuthFailed: '키를 확인하지 못했습니다. 확인 후 다시 시도하세요.', + onboardingTimeout: '확인 시간이 초과되었습니다. 네트워크 또는 프록시를 확인한 후 다시 시도하세요.', + onboardingNetwork: '모델 공급자에 연결할 수 없습니다. 네트워크, 프록시 또는 서비스 URL을 확인하세요.', + onboardingUnavailable: '모델 연결 서비스를 사용할 수 없습니다. 나중에 다시 시도하세요.', + onboardingInvalidResponse: '모델 공급자가 인식할 수 없는 응답을 반환했습니다. 나중에 다시 시도하세요.', + onboardingCatalogFull: '연결 한도에 도달했습니다. 먼저 사용하지 않는 연결을 삭제하세요.', + onboardingModelsChanged: '사용 가능한 모델이 변경되었습니다. 다시 확인하고 새로 선택하세요.', + onboardingNoModels: '사용할 수 있는 모델을 찾지 못했습니다. 연결을 만들지 않았습니다.', + onboardingSelectModel: '모델을 하나 이상 활성화하세요.', + onboardingOutcomeUnknown: '저장 결과를 확인할 수 없습니다', + onboardingOutcomeUnknownDetail: '중복 연결이 생성될 수 있으므로 아직 다시 추가하지 마세요. 연결 목록을 새로 고쳐 이 연결이 표시되는지 확인하고, 결과가 여전히 불분명하면 Runtime Host를 다시 연결하세요.', + onboardingReloadConnections: '연결 목록 새로 고침', + onboardingRestart: '그래도 다른 연결 추가', + capabilities: "기능", + thinkingEffort: "사고 수준(reasoning_effort)", + thinkingEffortHelp: "이 모델이 지원하는 사고 수준을 선택하세요. 체크 표시가 없으면 선언되지 않았음을 의미합니다.", + thinkingUndeclared: "선언되지 않음", + thinkingSelectedCount: (count: number) => `${count} 선택됨`, + thinkingBulk: "모든 모델의 사고 수준 설정", + thinkingBulkHelp: "선택하면 아래의 활성화된 모든 모델에 레벨이 추가됩니다. 선택을 취소하면 모든 항목에서 해당 항목이 제거됩니다. 다른 레벨은 그대로 둡니다.", + thinkingBulkTrigger: "모든 모델에 적용", + thinkingBulkCoverage: (declared: number, total: number) => declared === 0 ? "모델 없음" : `${declared} 모델의 ${total}에서`, + visionInput: "비전 입력", + visionInputHelp: "자동은 내장된 메타데이터를 따릅니다. 활성화/비활성화는 이를 명시적으로 재정의합니다.", + visionAuto: "자동", + visionEnabledOption: "활성화됨", + visionDisabledOption: "장애가 있는", + contextWindow: "컨텍스트 창(토큰)", + contextWindowHelp: "설정되면 압축 및 예산이 이 값을 사용합니다. 비어 있으면 내장된 메타데이터가 결정합니다.", + saveCapabilities: "기능 선언 저장", + fastMode: "빠른 모드", + fastModeHelp: "OpenAI의 빠른 서비스 계층을 사용하세요. 비어 있음은 공급자 기본값을 따릅니다.", + fastAuto: "자동", + fastEnabled: "빠른" + }, + oauthFlow: { + refreshFailed: '로그인 상태를 새로 고치지 못했습니다', + accountActionFailed: (name: string) => `${name} 계정 작업 실패`, + loginFailedRetry: '로그인에 실패했습니다. 나중에 다시 시도하세요.', + retry: "나중에 다시 시도하세요.", + startFailed: '로그인을 시작하지 못했습니다', + startFailedRetry: '로그인을 시작하지 못했습니다. 나중에 다시 시도하세요.', + openFailed: '브라우저를 열지 못했습니다', + openFailedRetry: '브라우저를 열지 못했습니다. 다시 시도하세요.', + loginSuccess: '로그인됨', + bound: (name: string) => `${name}이(가) 이 장치에 연결되었습니다.`, + incomplete: '로그인 미완료', + incompleteRetry: '로그인이 완료되지 않았습니다. 브라우저 인증을 다시 열고 시도하세요.', + loginFailed: '로그인 실패', + logoutDescription: '로컬에 저장된 구독 자격 증명을 제거합니다. 이 OAuth 모델을 사용하려면 다시 로그인해야 합니다.', + logout: '로그아웃', + cancel: "취소", + loggedOut: '로그아웃됨', + credentialsCleared: '로컬 자격 증명이 삭제되었습니다.', + logoutFailed: '로그아웃 실패', + logoutFailedRetry: '로그아웃에 실패했습니다. 나중에 다시 시도하세요.', + reverifyFailedRetry: '확인에 실패했습니다. 나중에 다시 시도하세요.', + serviceUnavailable: '로그인 서비스를 일시적으로 사용할 수 없습니다. 네트워크를 확인한 후 다시 시도하세요.', + logoutTitle: (name: string) => `${name}에서 로그아웃하시겠습니까?` + }, + oauthSection: { + signedIn: '로그인됨', + codexDescription: 'ChatGPT Plus / Pro 계정을 사용하여 연결을 추가하세요.', + xaiDescription: 'SuperGrok 또는 X Premium 계정을 사용하여 연결을 추가하세요.', + configuredConnections: (count: number) => `${count}개 연결 구성됨 · 다른 계정 추가`, + copilotDescription: 'GitHub에 로그인하여 Copilot 구독을 연결하거나 호환되는 자격 증명을 가져오세요.', + serviceUnavailable: '로그인 서비스를 일시적으로 사용할 수 없습니다. 네트워크를 확인한 후 다시 시도하세요.', + aria: 'OAuth 로그인', + staleState: 'OAuth 로그인 상태를 새로 고치지 못했습니다. 마지막으로 알려진 상태를 유지합니다. ', + codexDetail: '아래 장치 페이지를 열고 여기에 표시된 로그인 코드를 입력하세요.', + xaiDetail: '아래 브라우저를 열어 로그인하세요. 인증 결과가 자동으로 반영됩니다.', + deviceCode: '로그인 코드:', + openingBrowser: '브라우저 여는 중…', + waitingAuthorization: '브라우저 인증 대기 중…', + logout: '로그아웃', + loggingOut: '로그아웃 중…', + copilotSubtitle: 'GitHub에 로그인하거나 호환되는 자격 증명을 가져옵니다. 토큰은 렌더러에 노출되지 않습니다.', + copilotImported: 'GitHub Copilot 구독 계정을 가져왔습니다.', + copilotSetup: 'GitHub에 로그인하여 Copilot을 연결하거나 기존 gh 자격 증명을 가져오세요.', + importing: "가져오는 중…", + copilotSignIn: 'GitHub로 로그인', + copilotActionFailed: 'GitHub Copilot 계정 작업 실패', + copilotSignInDisabledHint: '이 설치에서는 GitHub 로그인이 활성화되어 있지 않습니다. 대신 호환되는 자격 증명을 가져오거나 관리자에게 활성화를 요청하세요.', + reimport: "다시 가져오기", + importCredential: "호환되는 자격 증명 가져오기", + verifying: '확인 중…', + reverify: '다시 확인', + removing: '제거 중…', + removeLocal: '로컬 로그인 제거', + loadingAccount: '계정 상태 로드 중…', + authorizing: '브라우저 창에서 로그인을 완료하세요.', + refreshing: '액세스 토큰 새로 고침 중…', + refreshTokenFailed: '토큰을 새로 고치지 못했습니다. 다시 로그인하세요.', + cardAria: (intent: 'add' | 'import' | 'manage', name: string, status: string | undefined, description: string) => `${intent === 'add' ? '계정 추가' : intent === 'import' ? '계정 가져오기' : '계정 관리'}: ${name}${status ? `; 상태: ${status}` : ''}; ${description.replace(/[。.!!??]+$/u, '')}`, + connectTitle: (name: string) => `${name} 연결`, + addAccountTitle: (name: string) => `${name} 계정 추가`, + login: (name: string) => `${name}에 로그인`, + loginAndAdd: '로그인 후 추가', + signedOut: (name: string) => `${name}에서 로그아웃되었습니다.`, + storageFailed: (name: string) => `${name}의 로컬 자격 증명을 읽지 못했습니다. 다시 로그인하세요.`, + providerUnavailable: (name: string) => `${name}에 로그인되어 있지만 공급자 상태를 현재 사용할 수 없습니다.` + } + } +} satisfies UiCatalog; export function getProviderSettingsCopy(locale: UiLocale): ProviderSettingsCopy { return PROVIDER_SETTINGS_COPY[locale]; } diff --git a/apps/desktop/src/renderer/locales/artifact-copy.ts b/apps/desktop/src/renderer/locales/artifact-copy.ts index 97dc2ec0fa..d8abafe8fc 100644 --- a/apps/desktop/src/renderer/locales/artifact-copy.ts +++ b/apps/desktop/src/renderer/locales/artifact-copy.ts @@ -164,6 +164,111 @@ const ARTIFACT_COPY = { unsupported: 'Unsupported preview', name: 'Name', unnamed: '(unnamed)', type: 'Type', size: 'Size', openInFinder: 'Show in Finder', loadingImage: 'Loading image preview…', }, }, + ko: { + pane: { + refreshFailed: "생성된 파일을 새로 고치지 못했습니다.", + openFailed: "Finder에서 생성된 파일을 표시할 수 없습니다.", + copyFailed: "복사 실패", + readTextFailed: "생성된 파일을 텍스트로 읽을 수 없습니다.", + copied: "생성된 파일 텍스트가 복사되었습니다.", + saved: "생성된 파일이 다음 이름으로 저장됨", + saveFailed: "다른 이름으로 저장 실패", + fallbackName: "생성된 파일", + deleteTitle: name => `"${name}" 삭제`, + deleteDescription: "일시 삭제: 이 기록을 삭제된 것으로 표시하고 파일을 6시간 동안 복구 가능하게 유지합니다.", + delete: "삭제", + deleteReadOnly: "삭제(읽기 전용 파일)", + cancel: "취소", + deleted: name => `${name} 삭제됨`, + deleteFailed: name => `${name}을(를) 삭제하지 못했습니다.`, + panelAria: "생성된 파일 미리보기 패널", + listLoadFailed: "생성된 파일을 로드하지 못했습니다.", + retrying: "재시도 중…", + retry: "다시 해 보다", + listAria: "생성된 파일", + deletedBadge: "삭제됨", + previewNamed: name => `미리보기 ${name}`, + empty: "생성된 파일 없음", + emptyHint: "어시스턴트가 생성한 파일이 여기에 표시됩니다.", + back: "생성된 파일로 돌아가기", + moreActions: name => `${name}에 대한 추가 작업`, + openInFinder: "Finder에 표시", + saveAs: "다른 이름으로 저장", + copy: "복사", + saveFailures: { + not_found: "생성된 파일이 존재하지 않습니다.", + not_allowed: "생성된 파일이 경로 안전 검사에 실패했습니다.", + deleted: "삭제된 생성 파일은 저장할 수 없습니다.", + write_failed: "대상에 쓸 수 없습니다.", + default: "생성된 파일을 저장할 수 없습니다." + }, + actionFailed: "생성된 파일 작업이 실패했습니다. 나중에 다시 시도하세요." + }, + preview: { + loadingFile: "파일 미리보기 로드 중…", + loadingDiff: "차이점 미리보기 로드 중…", + loadingHtml: "HTML 미리보기 로드 중…", + externalLinks: count => `이 미리보기에서는 외부 링크가 비활성화되었습니다. · 외부 링크 ${count}개`, + frameTitle: name => `생성된 파일 미리보기 · ${name}`, + loadingPdf: "PDF 미리보기 로드 중…", + pdfFallback: "브라우저에 PDF 뷰어가 내장되어 있지 않은 경우 자세히 메뉴에서 \"Finder에 표시\"를 사용하세요.", + rendered: "시사", + source: "원천", + previewLimited: limit => `첫 번째 ${limit}을 표시합니다. 전체 파일을 열거나 저장하려면 자세히 메뉴를 사용하세요.`, + renderLimited: (limit, lines) => `응답성을 유지하기 위해 풍부한 미리보기는 첫 번째 ${limit} 및 ${lines} 줄로 제한됩니다. 전체 소스는 계속 사용 가능합니다.`, + highlightLimited: (limit, lines) => `응답성을 유지하기 위해 구문 강조 표시는 첫 번째 ${limit} 및 ${lines} 줄로 제한됩니다. 나머지는 일반 텍스트입니다.`, + diffLinesLimited: count => `${count} 미리보기의 반응성을 유지하기 위해 더 많은 줄이 숨겨졌습니다.`, + readFailed: { + title: "생성된 파일을 읽을 수 없습니다.", + description: "파일이 외부에서 삭제되었을 수 있습니다. 위치를 확인하려면 자세히 메뉴의 \"Finder에 표시\"를 사용하세요." + }, + notAllowed: { + title: "생성된 파일을 읽을 수 없습니다.", + description: "파일이 더 이상 허용된 생성 파일 디렉터리에 없기 때문에 경로 안전 확인에 실패했습니다." + }, + tooLarge: bytes => ({ + title: "파일이 미리보기 크기를 초과합니다.", + description: `${bytes}바이트가 텍스트 미리보기 제한을 초과합니다. 전체 파일을 열거나 저장하려면 자세히 메뉴를 사용하세요.` + }), + deleted: { + title: "생성된 파일이 삭제되었습니다.", + description: "미리보기가 중지되었습니다. 원본 파일을 검사하려면 \"Finder에 표시\"를 사용하세요." + }, + unsupportedMime: { + title: "지원되지 않는 파일 형식", + description: "이 생성된 파일의 MIME 유형은 인라인 미리보기에 허용되지 않습니다. \"Finder에 표시\" 또는 \"다른 이름으로 저장\"을 사용하세요." + } + }, + registry: { + kindDisallowed: { + title: "이 유형은 여기에서 미리 볼 수 없습니다.", + description: "생성된 파일은 패널에서 미리 볼 수 없습니다. \"Finder에 표시\"를 사용하세요." + }, + mimeDisallowed: { + title: "미리보기 형식은 지원되지 않습니다.", + description: "MIME 유형이 인식되었지만 현재 미리보기는 PNG/JPEG/GIF/WebP/AVIF만 지원합니다." + }, + unknownType: { + title: "파일 형식을 식별할 수 없습니다.", + description: "파일에 MIME 메타데이터가 없으며 확장자가 일치하지 않습니다. \"Finder에 표시\"를 사용하세요." + }, + oversize: { + title: "파일이 너무 커서 미리 볼 수 없음", + description: "큰 이미지가 메모리에 로드되는 것을 방지하기 위해 2MB가 넘는 파일은 여기에서 확장되지 않습니다." + }, + readFailed: { + title: "미리보기를 로드하지 못했습니다.", + description: "파일을 읽을 수 없습니다. 권한에 의해 삭제, 이동 또는 차단되었을 수 있습니다. \"Finder에 표시\"를 사용하여 검사하십시오." + }, + unsupported: "지원되지 않는 미리보기", + name: "이름", + unnamed: "(이름 없음)", + type: "유형", + size: "크기", + openInFinder: "Finder에 표시", + loadingImage: "이미지 미리보기 로드 중…" + } +} } satisfies UiCatalog; export function getArtifactCopy(locale: UiLocale): ArtifactCopy { diff --git a/apps/desktop/src/renderer/locales/browser-copy.ts b/apps/desktop/src/renderer/locales/browser-copy.ts index 237ffe6544..68e54dd019 100644 --- a/apps/desktop/src/renderer/locales/browser-copy.ts +++ b/apps/desktop/src/renderer/locales/browser-copy.ts @@ -93,6 +93,30 @@ const BROWSER_COPY = { title: 'Embedded browser', description: 'Enter an address, or ask the assistant to navigate and interact with a page.', }, + ko: { + unsupportedScheme: "내장된 브라우저는 HTTP 및 HTTPS 주소만 지원합니다.", + invalidUrl: "이 주소는 유효하지 않습니다. 확인하고 다시 시도해 보세요.", + openFailed: "주소를 열 수 없습니다.", + navigationFailed: "브라우저 탐색 실패", + navigationFailedDetail: "페이지를 열 수 없습니다. 나중에 다시 시도하세요.", + panelAria: "내장된 브라우저", + panelAriaWithTitle: title => `내장된 브라우저: ${title}`, + insecure: "이 사이트는 HTTP를 통해 제공되므로 연결이 암호화되지 않습니다.", + backAria: "브라우저로 돌아가기", + back: "뒤쪽에", + forwardAria: "브라우저에서 앞으로 이동", + forward: "앞으로", + stopAria: "페이지 로딩 중지", + refreshAria: "페이지 새로고침", + stop: "멈추다", + refresh: "새로고침", + addressAria: "브라우저 주소", + addressPlaceholder: "주소를 입력하고 Enter를 누르세요", + closeAria: "브라우저 페이지 닫기", + close: "페이지 닫기", + title: "내장된 브라우저", + description: "주소를 입력하거나 도우미에게 페이지를 탐색하고 상호작용하도록 요청하세요." +} } satisfies UiCatalog; export function getBrowserCopy(locale: UiLocale): BrowserCopy { diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts index 05fba0ff9c..4a81de2bd3 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -373,7 +373,7 @@ export interface DesktopConversationCopy { * Turning them into a sentence is a copy decision, so it happens here, once. * * The call-kind tables are typed against the core union, so a kind added to the - * runtime fails this file at compile time instead of reaching a Chinese panel + * runtime fails this file at compile time instead of reaching a locale panel * as `daily_review`. The table also labels decode-only historical kinds such as * `semantic_compact`; the Runtime no longer emits them. */ @@ -399,8 +399,19 @@ const EN_CALL_KIND: CallKindCopy = { daily_review: 'Daily review', }; +const KO_CALL_KIND: CallKindCopy = { + memory_extraction: '메모리 추출', + semantic_compact: '시맨틱 압축', + history_compact: '기록 압축', + goal_evaluation: '목표 평가', + session_title: '작업 제목', + session_recap: '작업 요약', + daily_review: '일일 검토', +}; + const ZH_PERMISSION_DECISION: Record = { allow: '已允许', deny: '已拒绝' }; const EN_PERMISSION_DECISION: Record = { allow: 'Allowed', deny: 'Denied' }; +const KO_PERMISSION_DECISION: Record = { allow: '허용됨', deny: '거부됨' }; const ZH_RECOVERED: Record = { completed: '已完成', parked: '已搁置' }; @@ -425,6 +436,15 @@ const EN_TURN_FAILURE: Record = { error: 'Run error', }; +const KO_TURN_FAILURE: Record = { + tool_failed: '도구 실패', + model_call_failed: '모델 호출 실패', + turn_aborted: '턴 중단됨', + turn_cancelled: '턴 취소됨', + turn_failed: '턴 실패', + error: '실행 오류', +}; + /** Trailing breakdown for the coverage notice, in each language's punctuation. */ function zhDetail(parts: readonly string[]): string { return parts.length > 0 ? `:${parts.join('、')}` : ''; @@ -434,6 +454,10 @@ function enDetail(parts: readonly string[]): string { return parts.length > 0 ? `: ${parts.join(', ')}` : ''; } +function koDetail(parts: readonly string[]): string { + return parts.length > 0 ? `: ${parts.join(', ')}` : ''; +} + const COPY = { zh: { actions: { stopFailedTitle: '停止失败', stopFailedFallback: '任务操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新任务列表失败', refreshSessionsFailedFallback: '刷新任务列表失败,请稍后重试。', conversationErrorTitle: '任务出错', conversationErrorFallback: '任务运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新任务 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原任务仍会保留;修改后发送将在新版本中继续', revisionReadyTitle: '可以修改并重发了', revisionReadyDescription: '已回到该消息之前;编辑后发送即可', revisionUnavailableTitle: '暂时无法编辑这条消息', revisionAttachmentsUnsupported: '包含附件的历史消息暂不支持编辑并重发,请复制文字后新建消息。', revisionTransformedTextUnsupported: '通过显式技能发送的历史消息暂不支持编辑并重发,请复制文字后重新选择技能。', revisionDraftAttachmentConflict: 'Composer 中已有待发送附件,请先发送或移除附件,再编辑历史消息。', revisionCommandUnsupported: '修改消息时不能执行 /compact、/side 或编排命令,请取消修改后再试。', revisionAlreadyActive: '已有一条消息正在修改,请先发送或取消当前修改。', revisionCancelLabel: '取消', revisionBannerTitle: '正在修改已发送消息', revisionBannerDetail: '· 发送后创建新版本', revisionUnchanged: '内容没有变化。如需重新回答,请使用“重新生成”。', operationFailedTitle: '操作失败', operationFailedFallback: '任务操作失败,请稍后重试。', attachmentFailedTitle: '添加附件失败', imageAttachmentNotDirectTitle: '图片已作为附件添加', imageAttachmentNotDirectDescription: '当前模型不会直接接收图片。图片已作为附件提供给模型。', tryAgain: '请稍后重试。', modelReboundTitle: '已切换到可用模型', modelReboundDescription: (modelId) => `原任务使用的连接已不可用${modelId ? ` · ${modelId}` : ''}`, messageReadFailedTitle: '读取任务失败', partialHistoryTitle: '正在查看较早的消息', returnLatest: '返回最新消息', scrollMainToBottom: '滚动主对话到底部' }, @@ -918,6 +942,504 @@ const COPY = { }, turnError: { unknown: 'Something went wrong, cause unknown. Send a message to retry.', contextOverflow: 'Context exceeded the model window. Reduce attachments or start a new task.', timeout: 'The model request timed out. Send a message to retry.', auth: 'Model authentication failed. Reconnect or sign in again from Settings.', providerBilling: 'Model billing is restricted. Check the account balance or subscription.', providerCapacity: 'The model service is temporarily at capacity. Wait a few minutes, or switch models.', rateLimit: 'Requests were rate-limited. Wait a moment, then send a message to retry.', network: 'The network connection failed. Check the network, then send a message again.', provider: 'The model service returned an error. Retry later, or switch models.', stepCap: 'The tool-step limit was reached, so the task may be incomplete. Send a message to continue.', tool: 'A tool call failed. Check the tool result above before deciding whether to retry.', permission: 'This turn ended while waiting for permission. Send a message and it will ask again.', restarted: 'The app restarted before the previous turn completed', sandboxBoundaryClosed: 'The app restarted, so the pending request to reach outside the workspace was closed as denied. Send a message to decide again.', executionState: { erroredTool: 'A tool errored during this turn. Read its result before deciding whether to send another message.', toolRan: 'Tools already ran during this turn and may have made real changes. Read their results before sending another message.', partialOutput: 'This turn produced part of an answer. Worth reading before you send another message.' } }, }, + ko: { + ...{ + actions: { + stopFailedTitle: "중지하지 못했습니다.", + stopFailedFallback: "작업 작업이 실패했습니다. 나중에 다시 시도하세요.", + refreshSessionsFailedTitle: "작업을 새로 고치지 못했습니다.", + refreshSessionsFailedFallback: "작업 목록을 새로 고칠 수 없습니다. 나중에 다시 시도하세요.", + conversationErrorTitle: "작업 오류", + conversationErrorFallback: "작업 실행이 실패했습니다. 나중에 다시 시도하세요.", + regenerateStartedTitle: "재생이 시작되었습니다", + regenerateStartedDescription: "새로운 응답 생성", + branchCreatedTitle: "지점이 생성되었습니다.", + branchCreatedDescription: name => `새 작업: ${name}`, + revisionStartedTitle: "초안 수정 준비됨", + revisionStartedDescription: "원래 작업은 유지됩니다. 보내면 새 버전이 생성됩니다.", + revisionReadyTitle: "편집하고 다시 보낼 준비가 되었습니다.", + revisionReadyDescription: "해당 메시지 이전으로 되감기합니다. 준비가 되면 편집하고 보내세요", + revisionUnavailableTitle: "이 메시지는 아직 편집할 수 없습니다.", + revisionAttachmentsUnsupported: "편집 및 재전송은 아직 기록 첨부 파일을 지원하지 않습니다. 대신 텍스트를 새 메시지에 복사하세요.", + revisionTransformedTextUnsupported: "편집 및 재전송은 아직 명시적인 기술로 전송된 메시지를 지원하지 않습니다. 대신 텍스트를 복사하고 스킬을 다시 선택하세요.", + revisionDraftAttachmentConflict: "작성자에게 이미 보류 중인 첨부 파일이 있습니다. 보낸 메시지를 편집하기 전에 보내거나 제거하세요.", + revisionCommandUnsupported: "보낸 메시지를 편집하는 동안에는 /compact, /side 또는 Orchestration 명령을 실행할 수 없습니다. 먼저 편집을 취소하세요.", + revisionAlreadyActive: "다른 메시지가 이미 편집 중입니다. 먼저 해당 수정사항을 보내거나 취소하세요.", + revisionCancelLabel: "취소", + revisionBannerTitle: "보낸 메시지 편집 중", + revisionBannerDetail: "· 전송 시 새 버전", + revisionUnchanged: "아무것도 바뀌지 않았습니다. 새로운 답변만 원할 경우 재생성을 사용하세요.", + operationFailedTitle: "작업 실패", + operationFailedFallback: "작업 작업이 실패했습니다. 나중에 다시 시도하세요.", + attachmentFailedTitle: "첨부파일을 추가하지 못했습니다.", + imageAttachmentNotDirectTitle: "첨부파일로 추가된 이미지", + imageAttachmentNotDirectDescription: "현재 모델은 이미지를 직접 수신하지 않습니다. 이미지는 첨부파일로 제공되었습니다.", + tryAgain: "나중에 다시 시도하세요.", + modelReboundTitle: "사용 가능한 모델로 전환됨", + modelReboundDescription: modelId => `이전 연결을 사용할 수 없습니다${modelId ? `· ${modelId}` : ''}`, + messageReadFailedTitle: "작업을 로드하지 못했습니다.", + partialHistoryTitle: "이전 메시지 보기", + returnLatest: "최신으로 돌아가기", + scrollMainToBottom: "기본 대화를 맨 아래로 스크롤" + }, + attachments: { + tooMany: "최대 8개의 파일을 첨부할 수 있습니다.", + tooLarge: "첨부파일은 50MB 이하여야 합니다.", + duplicate: "이 첨부파일은 이미 추가되었습니다." + }, + model: { + fakeBackendLabel: "로컬 시뮬레이션", + setupTitle: "실제 모델 구성", + connectionMissingTitle: "연결이 삭제되었습니다.", + configurationFallback: "이 모델 연결은 지금은 보낼 수 없습니다. 설정·모델에서 확인하고 다시 시도해 보세요.", + configurationReason: { + missing_default_connection: "보내기 전에 설정 · 모델에서 기본 모델을 설정하세요.", + connection_missing: "이 작업에서 사용된 모델 연결이 삭제되었습니다. 설정 · 모델에서 선택하거나 생성하세요.", + connection_disabled: "현재 모델 연결이 비활성화되었습니다. 활성화하거나 설정 · 모델에서 다른 기본값을 선택하십시오.", + missing_api_key: "현재 모델 연결에는 사용 가능한 자격 증명이 없습니다. API 키를 추가하거나 설정 · 모델에서 다시 로그인하세요.", + missing_model: "현재 연결에는 사용 가능한 모델이 없습니다. 설정 · 모델에서 기본 모델을 선택하세요.", + empty_model_list: "현재 연결에는 활성화된 모델이 없습니다. 설정 · 모델에서 추가하거나 활성화하세요.", + model_not_enabled: "이 작업을 위해 선택한 모델이 비활성화되었습니다. 설정 · 모델에서 활성화된 모델을 선택하세요.", + model_not_chat_capable: "이 작업을 위해 선택한 모델은 채팅을 할 수 없습니다. 설정 · 모델에서 채팅 가능 모델을 선택하세요.", + fake_backend: "이 작업에서는 폐기된 로컬 시뮬레이션을 사용했습니다. 설정·모델에서 실제 모델을 추가한 후 새 작업을 시작하세요.", + provider_retired: "이 작업의 연결에 사용되는 로그인이 Maka에서 제거되었으므로 전송할 수 없습니다. 설정 · 모델에서 다른 연결로 전환한 다음 새 작업을 시작하세요." + } + }, + footer: { + labels: { + regenerate: "재생성", + branch: "나뭇가지", + copy: "복사", + info: "세부" + }, + pending: "일하고 있는…", + regenerateRunning: "재생성하기 전에 현재 응답이 완료될 때까지 기다립니다.", + regenerateAgain: "재생성된 응답이 이미 존재합니다. 또 다른 병렬 응답을 생성하려면 다시 클릭하세요.", + regenerate: "이번 턴에 또 다른 응답을 생성합니다.", + requestRegenerate: "소유자에게 이 응답 재생성을 승인해 달라고 요청합니다.", + branchRunning: "분기하기 전에 현재 응답이 완료될 때까지 기다립니다.", + branchAborted: "중단 전 컨텍스트에서 분기", + branch: "이 응답에서 새 작업을 분기합니다.", + copy: "응답을 클립보드에 복사", + copyEmpty: "이 응답에는 복사할 콘텐츠가 없습니다." + }, + lineage: { + regeneratedFrom: "이전 응답에서 재생성됨", + regeneratedFromTooltip: "이는 병렬로 재생성된 응답입니다. 보관된 이전 응답을 보려면 클릭하세요.", + regeneratedTo: "재생성 → 새로운 응답", + regeneratedToTooltip: "재생성된 응답으로 이동" + }, + workbar: { + ariaLabel: "작업 작업 표시줄", + sectionsAriaLabel: "작업 작업 표시줄 탭", + review: "변경 사항", + terminal: "단말기", + terminalNumbered: index => `터미널 ${index}`, + tasks: "할 일", + todoLoadFailed: "할 일 목록을 로드하지 못했습니다. 다시 시도해 보세요.", + workBoard: "작업판", + browser: "브라우저", + files: "생성된 파일", + inspector: "추적하다", + sideChat: "사이드 채팅", + sideChatNumbered: index => `사이드 채팅 ${index}`, + openTab: "작업 표시줄 탭 열기", + openTools: "도구 열기", + closeTab: label => `${label} 닫기`, + tabMenu: label => `${label} 탭 메뉴`, + moveLeft: "왼쪽으로 이동", + moveRight: "오른쪽으로 이동", + moveToRight: "오른쪽 패널로 이동", + moveToBottom: "하단 패널로 이동", + pinTab: "핀 탭", + pinTabHint: "미리보기 탭. 콘텐츠를 두 번 클릭하거나 상호작용하여 고정하세요.", + close: "닫다", + closeOthers: "다른 탭 닫기", + closeToRight: "오른쪽 탭 닫기", + launcher: { + review: "현재 Git 작업공간의 변경사항 보기", + terminal: "이 작업에 대한 터미널 실행 및 실시간 출력을 검사합니다.", + tasks: "이 작업의 할 일 원장을 보고 유지합니다.", + workBoard: "지연된 작업 캡처 및 관리", + browser: "내장된 브라우저를 열고 현재 페이지를 유지합니다.", + files: "이 작업으로 생성된 파일 찾아보기", + inspector: "모델 호출, 도구, 타이밍 검사", + sideChat: "주요 작업을 중단하지 않고 읽기 전용으로 질문하고 탐색하세요." + } + }, + workBoardPanel: { + inbox: "받은편지함", + project: "현재 프로젝트", + noProject: "선택한 프로젝트가 없습니다.", + createPlaceholder: "나중을 위해 무언가를 캡처하세요...", + create: "추가하다", + empty: "연기된 작업 없음", + loading: "작업 보드 로드 중…", + loadMore: "더 로드하기", + retry: "다시 해 보다", + loadFailed: "작업 보드를 로드하지 못했습니다.", + actionFailed: "작업 실패", + complete: "완벽한", + reopen: "다시 열다", + rename: "이름 바꾸기", + renameSave: "구하다", + moveToInbox: "받은편지함으로 이동", + moveToProject: "프로젝트로 이동", + archive: "보관소", + unarchive: "복원하다", + delete: "삭제", + archived: "보관됨" + }, + reviewPanel: { + ariaLabel: "힘내 변경", + empty: "현재 Git 작업공간에는 변경사항이 없습니다.", + emptyHelp: "커밋되고, 준비되고, 수정된 파일이 여기에 표시됩니다.", + notGitRepository: "이 작업 디렉터리는 Git 저장소가 아닙니다.", + workspaceUnavailable: "이 작업 디렉터리를 사용할 수 없습니다.", + unbornRepository: "이 Git 저장소에는 아직 비교할 커밋이 없습니다.", + gitFailed: "Git 작업공간 변경사항을 읽을 수 없습니다.", + invalidBaseBranch: "선택한 비교 분기를 사용할 수 없습니다.", + truncated: "변경사항이 너무 많습니다. 첫 번째 파일만 표시", + showMore: remaining => `${Math.min(20, remaining)}개의 파일 더 표시`, + hiddenLines: count => `${count}개의 추가 줄이 표시되지 않음`, + changedFiles: count => `${count}개의 파일이 변경됨`, + addedLines: count => `${count}줄 추가됨`, + deletedLines: count => `${count}줄 삭제됨`, + added: count => `${count} 추가됨`, + deleted: count => `${count} 삭제됨`, + loadFailed: "Git 변경 사항을 읽을 수 없습니다.", + retry: "다시 해 보다" + }, + terminalPanel: { + ariaLabel: "태스크 터미널", + empty: "이 작업에서는 아직 실행되는 터미널이 없습니다.", + emptyHelp: "이 작업의 터미널은 시작되면 여기에 표시됩니다.", + loadFailed: "터미널 실행을 읽을 수 없습니다.", + retry: "다시 해 보다", + refresh: "터미널 새로 고침", + readOnly: "이 작업에서 에이전트 또는 사용자가 시작한 터미널 실행을 표시합니다.", + runCount: count => `${count}개의 터미널 실행`, + newTerminal: "새로운 터미널", + commandPlaceholder: "명령을 입력하고 Enter를 누르세요", + commandLabel: "터미널 명령", + runCommand: "명령 실행", + stopTerminal: "현재 터미널 중지", + startFailed: "터미널을 시작할 수 없습니다", + writeFailed: "터미널 입력을 보낼 수 없습니다.", + stopFailed: "터미널을 중지할 수 없습니다." + }, + inspector: { + ariaLabel: "태스크 추적", + copyPricingKey: "가격 키 복사", + pricingKeyCopied: "가격 키가 복사되었습니다.", + unpricedPricingKey: "가격이 책정되지 않은 가격 책정 키", + copyFailed: "복사 실패", + copyFailedDetail: "클립보드를 사용할 수 없거나 시스템에서 액세스를 거부했습니다.", + loadFailed: "추적을 읽을 수 없습니다.", + retry: "다시 해 보다", + empty: "이 작업에서는 아직 추적할 항목이 없습니다.", + emptyHelp: "이 작업에 대해 아직 기록된 활동이 없습니다.", + costUnavailable: "비용을 알 수 없음", + costEstimateHelp: "기록된 사용량 및 가격을 바탕으로 추정됩니다. 누락되거나 가격이 책정되지 않은 통화는 제외될 수 있습니다.", + loadEarlier: "이전 레코드 로드", + hideEarlier: "이전 기록 모두 숨기기", + loadingEarlier: "로드 중…", + loadingTrace: "타임라인 로드 중…", + loadingSummary: "전체 세션 사용량 추정 중…", + summaryUnavailable: "전체 세션을 일시적으로 사용할 수 없습니다.", + totals: { + cost: "예상 비용" + }, + coveragePartial: parts => `일부 호출을 완전히 표시할 수 없어 아래 숫자는 ${koDetail(parts)}보다 적게 집계됩니다.`, + coverageAbsent: parts => `이 백엔드는 호출별 세부 정보를 기록하지 않습니다${koDetail(parts)}`, + unreadable: count => `${count}개 레코드를 읽을 수 없습니다.`, + oversizedRuns: count => `${count}개 실행 기록이 너무 커서 온라인으로 표시할 수 없습니다.`, + turnsMissing: count => `호출 기록이 없는 턴 ${count}개`, + turnsShort: count => `호출 기록이 불완전한 턴 ${count}개`, + stepKind: { + permission: "허가", + compaction: "컨텍스트 압축", + error: "오류" + }, + callKind: kind => KO_CALL_KIND[kind as keyof CallKindCopy] ?? kind, + permissionDecision: decision => KO_PERMISSION_DECISION[decision] ?? decision, + recoveredAs: disposition => `${disposition}로 복구됨`, + retries: count => `${count}회 재시도`, + turnFailure: code => KO_TURN_FAILURE[code] ?? '턴 실패', + turnLabel: startedAt => `턴 · ${startedAt}`, + overview: { + context: "컨텍스트 창", + segment: { + cacheRead: "캐시 적중", + fresh: "캐시 미스", + used: "사용된", + free: "남은" + }, + cacheHit: "캐시 적중률", + timelineTab: "타임라인", + composition: { + title: "예상 구성", + basis: "공급자가 보고한 토큰이 아닌 요청 바이트에서 추정됩니다.", + part: { + system_instructions: "시스템 지침", + tool_definitions: "도구 정의", + messages: "메시지", + other: "기타 옵션" + }, + tools: "도구별", + remainingTools: count => `${count}개의 추가 도구`, + unlabelled: "이름이 없는 도구", + unrecorded: "이 통화에는 기록이 남지 않았습니다." + } + } + }, + quoteCompanion: { + namePrefix: "옆:", + permissionStreaming: "사이드 채팅이 실행되는 동안에는 권한을 변경할 수 없습니다.", + scrollToBottom: "사이드 대화를 맨 아래로 스크롤", + compactSuccessTitle: "컨텍스트가 압축되었습니다", + compactSuccessDescription: "이전 컨텍스트가 체크포인트 요약으로 대체되었습니다.", + compactStartedTitle: "컨텍스트 압축 중", + compactStartedDescription: "이전 컨텍스트를 체크포인트로 요약하는 중입니다.", + compactUnchangedTitle: "압축할 내용이 없습니다", + compactUnchangedDescription: "이 작업은 이미 최신 체크포인트를 사용하고 있습니다.", + compactErrorTitle: "압축하지 못했습니다", + compactErrorFallback: "작업의 컨텍스트를 압축하지 못했습니다. 나중에 다시 시도하세요.", + workspaceUnavailableTitle: "작업 디렉터리를 사용할 수 없습니다", + workspaceUnavailableDescription: "작업 디렉터리가 없거나 접근할 수 없습니다. 새 작업에 사용할 폴더를 선택하세요.", + closeConfirmation: { + title: count => count > 1 ? `${count} 사이드 채팅을 종료하시겠습니까?` : "사이드 채팅을 닫으시겠어요?", + description: count => count > 1 ? `이러한 ${count} 임시 사이드 채팅은 영구적으로 삭제되며 복구할 수 없습니다.` : "이 임시 사이드 채팅은 영구적으로 삭제되며 복구할 수 없습니다.", + dontAskAgain: "다시 묻지 마세요", + cancel: "취소", + confirm: "사이드 채팅 닫기" + }, + errors: { + forkSetupFailed: "사이드 채팅을 열 수 없습니다. 다시 시도해 주세요.", + forkSourceBusy: "기본 대화 또는 연결된 작업이 아직 실행 중입니다. 완료되면 다시 시도하세요.", + forkUnsupported: "이 대화 컨텍스트는 아직 사이드 채팅으로 열 수 없습니다.", + sendRejected: "동반자를 시작할 수 없습니다. 다시 시도해 주세요.", + sendFailed: "컴패니언 요청이 실패했습니다. 다시 시도해 주세요.", + settlementFailed: "실행이 종료되었지만 해당 메시지를 로드할 수 없습니다. 사이드 채팅을 다시 시도하거나 다시 열어보세요.", + respondFailed: "응답이 실패했습니다. 다시 시도해 주세요." + } + }, + health: { + blocked: { + fake_backend: { + label: "오래된 작업 · 실제 모델 구성", + tooltip: () => "이 작업에서는 폐기된 로컬 시뮬레이션을 사용했습니다. 보내기 전에 설정 · 모델에서 실제 모델을 추가하고 활성화하세요." + }, + provider_retired: { + label: "로그인이 중단됨", + tooltip: name => `"${name}" 연결이 사용하는 로그인이 Maka에서 제거되었으므로 전송에 실패합니다. 설정 · 모델에서 다른 연결로 전환하십시오.` + }, + missing_default_connection: { + label: "구성된 모델이 없습니다.", + tooltip: () => "이 작업에는 사용 가능한 모델 연결이 없습니다. 설정 · 모델에서 하나를 추가하고 활성화하세요." + }, + legacy_connection_identity: { + label: "모델 연결 선택", + tooltip: () => "이 작업은 이전 버전에서 가져온 것입니다. 사용할 연결과 모델을 선택하세요.", + actionLabel: "연결 및 모델 선택", + settingsTooltip: () => "현재 사용 가능한 연결이 없습니다. 먼저 설정 · 모델에서 하나를 추가하거나 활성화하세요." + }, + connection_missing: { + label: "원래 연결이 삭제되었습니다.", + tooltip: () => "계속하려면 새 연결과 모델을 선택하세요.", + actionLabel: "연결 및 모델 선택", + settingsTooltip: () => "현재 사용 가능한 연결이 없습니다. 먼저 설정 · 모델에서 하나를 추가하거나 활성화하세요." + }, + connection_identity_mismatch: { + label: "연결 ID 불일치", + tooltip: () => "다시 사용할 연결과 모델을 선택하세요.", + actionLabel: "연결 및 모델 선택", + settingsTooltip: () => "현재 사용 가능한 연결이 없습니다. 먼저 설정 · 모델에서 하나를 추가하거나 활성화하세요." + }, + connection_disabled: { + label: "연결이 비활성화되었습니다.", + tooltip: name => `연결 "${name}"이 비활성화되었습니다. 활성화하거나 설정 · 모델에서 다른 연결을 선택하십시오.` + }, + missing_api_key: { + label: "연결 자격 증명이 누락되었습니다.", + tooltip: name => `연결 "${name}"에 API 키가 없거나 로그인이 완료되었습니다. 설정 · 모델에서 자격 증명을 추가합니다.` + }, + missing_model: { + label: "선택한 모델이 없습니다.", + tooltip: name => `연결 "${name}"에는 기본 모델이 없습니다. 설정 · 모델에서 하나를 선택하세요.` + }, + empty_model_list: { + label: "활성화된 모델이 없습니다.", + tooltip: name => `연결 "${name}"에는 활성화된 모델이 없습니다. 설정 · 모델에서 하나를 추가하세요.` + }, + model_not_enabled: { + label: "작업 모델이 비활성화되었습니다.", + tooltip: (name, model) => `모델 "${model}"은 "${name}" 연결에 대해 활성화되지 않습니다. 설정 · 모델에서 다른 모델을 선택하십시오.` + }, + model_not_chat_capable: { + label: "작업 모델은 채팅할 수 없습니다.", + tooltip: (_name, model) => `모델 "${model}"은(는) 채팅에 사용할 수 없습니다. 설정 · 모델에서 채팅 가능 모델을 선택하세요.` + } + }, + connectionChoicesLoading: { + tooltip: "연결 목록이 아직 로드되지 않았습니다.", + actionLabel: "연결 다시 로드" + }, + reauth: { + label: "마지막 연결 테스트 실패 인증", + tooltip: "최신 테스트에서는 401/403이 반환되었습니다. 전송이 차단되지는 않지만 실패할 경우 설정 · 모델에서 다시 로그인하세요." + }, + testError: { + label: "마지막 연결 테스트 실패", + tooltip: "네트워크, 시간 초과 또는 5xx 오류로 인해 최근 테스트가 실패했습니다. 전송이 차단되지 않습니다. 지속되는 경우 기본 URL 또는 프록시 설정을 확인하세요." + } + }, + turnError: { + unknown: "문제가 발생했습니다. 원인을 알 수 없습니다. 다시 시도하려면 메시지를 보내세요.", + contextOverflow: "컨텍스트가 모델 창을 초과했습니다. 첨부 파일을 줄이거나 새 작업을 시작하세요.", + timeout: "모델 요청 시간이 초과되었습니다. 다시 시도하려면 메시지를 보내세요.", + auth: "모델 인증에 실패했습니다. 설정에서 다시 연결하거나 로그인하세요.", + providerBilling: "모델 과금이 제한됩니다. 계정 잔액이나 구독을 확인하세요.", + providerCapacity: "모델 서비스가 일시적으로 중단되었습니다. 몇 분 정도 기다리거나 모델을 전환하십시오.", + rateLimit: "요청 속도가 제한되었습니다. 잠시 기다린 후 메시지를 보내 다시 시도하세요.", + network: "네트워크 연결에 실패했습니다. 네트워크를 확인한 후 메시지를 다시 보내세요.", + provider: "모델 서비스에서 오류를 반환했습니다. 나중에 다시 시도하거나 모델을 전환하세요.", + stepCap: "도구 단계 제한에 도달했으므로 작업이 완료되지 않았을 수 있습니다. 계속하려면 메시지를 보내세요.", + tool: "도구 호출이 실패했습니다. 재시도 여부를 결정하기 전에 위의 도구 결과를 확인하세요.", + permission: "이번 턴은 허가를 기다리는 동안 종료되었습니다. 메시지를 보내면 다시 물어볼 것입니다.", + restarted: "이전 차례가 완료되기 전에 앱이 다시 시작되었습니다.", + sandboxBoundaryClosed: "앱이 다시 시작되었으므로 작업 영역 외부에 도달하기 위해 보류 중인 요청이 거부되어 종료되었습니다. 다시 결정하려면 메시지를 보내세요.", + executionState: { + erroredTool: "이번 턴에 도구에 오류가 발생했습니다. 다른 메시지를 보낼지 여부를 결정하기 전에 결과를 읽어보세요.", + toolRan: "이번 차례에 도구가 이미 실행되었으며 실질적인 변경이 이루어졌을 수도 있습니다. 다른 메시지를 보내기 전에 결과를 읽어보세요.", + partialOutput: "이번 차례에서는 답변의 일부가 나왔습니다. 다른 메시지를 보내기 전에 읽어볼 가치가 있습니다." + } + } + }, + actions: { + ...{ + stopFailedTitle: "중지하지 못했습니다.", + stopFailedFallback: "작업 작업이 실패했습니다. 나중에 다시 시도하세요.", + refreshSessionsFailedTitle: "작업을 새로 고치지 못했습니다.", + refreshSessionsFailedFallback: "작업 목록을 새로 고칠 수 없습니다. 나중에 다시 시도하세요.", + conversationErrorTitle: "작업 오류", + conversationErrorFallback: "작업 실행이 실패했습니다. 나중에 다시 시도하세요.", + regenerateStartedTitle: "재생이 시작되었습니다", + regenerateStartedDescription: "새로운 응답 생성", + branchCreatedTitle: "지점이 생성되었습니다.", + branchCreatedDescription: name => `새 작업: ${name}`, + revisionStartedTitle: "초안 수정 준비됨", + revisionStartedDescription: "원래 작업은 유지됩니다. 보내면 새 버전이 생성됩니다.", + revisionReadyTitle: "편집하고 다시 보낼 준비가 되었습니다.", + revisionReadyDescription: "해당 메시지 이전으로 되감기합니다. 준비가 되면 편집하고 보내세요", + revisionUnavailableTitle: "이 메시지는 아직 편집할 수 없습니다.", + revisionAttachmentsUnsupported: "편집 및 재전송은 아직 기록 첨부 파일을 지원하지 않습니다. 대신 텍스트를 새 메시지에 복사하세요.", + revisionTransformedTextUnsupported: "편집 및 재전송은 아직 명시적인 기술로 전송된 메시지를 지원하지 않습니다. 대신 텍스트를 복사하고 스킬을 다시 선택하세요.", + revisionDraftAttachmentConflict: "작성자에게 이미 보류 중인 첨부 파일이 있습니다. 보낸 메시지를 편집하기 전에 보내거나 제거하세요.", + revisionCommandUnsupported: "보낸 메시지를 편집하는 동안에는 /compact, /side 또는 Orchestration 명령을 실행할 수 없습니다. 먼저 편집을 취소하세요.", + revisionAlreadyActive: "다른 메시지가 이미 편집 중입니다. 먼저 해당 수정사항을 보내거나 취소하세요.", + revisionCancelLabel: "취소", + revisionBannerTitle: "보낸 메시지 편집 중", + revisionBannerDetail: "· 전송 시 새 버전", + revisionUnchanged: "아무것도 바뀌지 않았습니다. 새로운 답변만 원할 경우 재생성을 사용하세요.", + operationFailedTitle: "작업 실패", + operationFailedFallback: "작업 작업이 실패했습니다. 나중에 다시 시도하세요.", + attachmentFailedTitle: "첨부파일을 추가하지 못했습니다.", + imageAttachmentNotDirectTitle: "첨부파일로 추가된 이미지", + imageAttachmentNotDirectDescription: "현재 모델은 이미지를 직접 수신하지 않습니다. 이미지는 첨부파일로 제공되었습니다.", + tryAgain: "나중에 다시 시도하세요.", + modelReboundTitle: "사용 가능한 모델로 전환됨", + modelReboundDescription: modelId => `이전 연결을 사용할 수 없습니다${modelId ? `· ${modelId}` : ''}`, + messageReadFailedTitle: "작업을 로드하지 못했습니다.", + partialHistoryTitle: "이전 메시지 보기", + returnLatest: "최신으로 돌아가기", + scrollMainToBottom: "기본 대화를 맨 아래로 스크롤" + }, + conversationErrorTitle: '대화 오류', + conversationErrorFallback: '작업 실행에 실패했습니다. 나중에 다시 시도하세요.', + regenerateStartedTitle: '재생성 시작됨', + regenerateStartedDescription: '새 응답을 생성하는 중입니다', + branchCreatedTitle: '분기 생성됨', + branchCreatedDescription: name => '새 작업: ' + name, + returnLatest: '최신 항목으로 돌아가기', + scrollMainToBottom: '주 대화를 아래로 스크롤' + }, + footer: { + ...{ + labels: { + regenerate: "재생성", + branch: "나뭇가지", + copy: "복사", + info: "세부" + }, + pending: "일하고 있는…", + regenerateRunning: "재생성하기 전에 현재 응답이 완료될 때까지 기다립니다.", + regenerateAgain: "재생성된 응답이 이미 존재합니다. 또 다른 병렬 응답을 생성하려면 다시 클릭하세요.", + regenerate: "이번 턴에 또 다른 응답을 생성합니다.", + branchRunning: "분기하기 전에 현재 응답이 완료될 때까지 기다립니다.", + branchAborted: "중단 전 컨텍스트에서 분기", + branch: "이 응답에서 새 작업을 분기합니다.", + copy: "응답을 클립보드에 복사", + copyEmpty: "이 응답에는 복사할 콘텐츠가 없습니다." + }, + labels: { + regenerate: '재생성', + branch: '분기', + copy: '복사', + info: '세부 정보' + }, + pending: '작업 중…', + requestRegenerate: '소유자에게 이 응답 재생성을 승인해 달라고 요청합니다.' + }, + workbar: { + ...{ + ariaLabel: "작업 작업 표시줄", + sectionsAriaLabel: "작업 작업 표시줄 탭", + review: "변경 사항", + terminal: "단말기", + terminalNumbered: index => `터미널 ${index}`, + tasks: "할 일", + todoLoadFailed: "할 일 목록을 로드하지 못했습니다. 다시 시도해 보세요.", + workBoard: "작업판", + browser: "브라우저", + files: "생성된 파일", + inspector: "추적하다", + sideChat: "사이드 채팅", + sideChatNumbered: index => `사이드 채팅 ${index}`, + openTab: "작업 표시줄 탭 열기", + openTools: "도구 열기", + closeTab: label => `${label} 닫기`, + tabMenu: label => `${label} 탭 메뉴`, + moveLeft: "왼쪽으로 이동", + moveRight: "오른쪽으로 이동", + moveToRight: "오른쪽 패널로 이동", + moveToBottom: "하단 패널로 이동", + pinTab: "핀 탭", + pinTabHint: "미리보기 탭. 콘텐츠를 두 번 클릭하거나 상호작용하여 고정하세요.", + close: "닫다", + closeOthers: "다른 탭 닫기", + closeToRight: "오른쪽 탭 닫기", + launcher: { + review: "현재 Git 작업공간의 변경사항 보기", + terminal: "이 작업에 대한 터미널 실행 및 실시간 출력을 검사합니다.", + tasks: "이 작업의 할 일 원장을 보고 유지합니다.", + workBoard: "지연된 작업 캡처 및 관리", + browser: "내장된 브라우저를 열고 현재 페이지를 유지합니다.", + files: "이 작업으로 생성된 파일 찾아보기", + inspector: "모델 호출, 도구, 타이밍 검사", + sideChat: "주요 작업을 중단하지 않고 읽기 전용으로 질문하고 탐색하세요." + } + }, + ariaLabel: '작업 워크바', + review: '변경 사항', + terminal: '터미널', + tasks: '할 일', + browser: '브라우저', + files: '생성된 파일', + inspector: '추적', + sideChat: '사이드 채팅' + } +} } satisfies UiCatalog; export function getDesktopConversationCopy(locale: UiLocale): DesktopConversationCopy { diff --git a/apps/desktop/src/renderer/locales/external-session-import-copy.ts b/apps/desktop/src/renderer/locales/external-session-import-copy.ts index 76e6265918..5392f2f09c 100644 --- a/apps/desktop/src/renderer/locales/external-session-import-copy.ts +++ b/apps/desktop/src/renderer/locales/external-session-import-copy.ts @@ -205,6 +205,59 @@ const COPY = { importOutcomeUnknownDescription: (names) => `Maka could not confirm the outcome of these imports: ${names.map((name) => `“${name}”`).join(', ')}. Look in the task list first, and do not import again anything that is already there.`, }, + ko: { + sourceLabel: "원천", + sourceNames: { + codex: "사본", + 'claude-code': "클로드 코드" + }, + includeArchived: "보관된 대화 포함", + searchLabel: "찾다", + searchHelp: "대화 제목과 프로젝트 경로가 일치합니다. 비어 있으면 모든 것이 표시됩니다.", + searchPlaceholder: "제목 또는 경로의 일부", + searchEmpty: term => `제목이나 경로에 "${term}"이 포함된 대화가 없습니다.`, + loading: "외부 대화를 읽는 중…", + listAria: "가져올 수 있는 대화", + emptyTitle: "가져올 대화가 없습니다.", + emptyDescription: "이 소스에는 일치하는 루트 대화가 없습니다.", + unavailableTitle: "지원되는 에이전트가 감지되지 않았습니다.", + unavailableDescription: "이 컴퓨터에서 Codex를 사용하면 해당 대화가 여기에 표시됩니다. Maka는 해당 파일을 읽기만 하고 수정하지 않습니다.", + loadFailedTitle: "외부 대화를 읽을 수 없습니다.", + loadFailedFallback: "외부 세션 디렉터리를 일시적으로 사용할 수 없습니다. 다시 시도해 보세요.", + retry: "다시 해 보다", + archived: "보관됨", + loadMore: "더 로드하기", + loadingMore: "로드 중…", + duplicateNote: "동일한 대화를 다시 가져오면 독립적인 작업이 생성됩니다.", + importedCount: count => count === 1 ? "한 번 가져옴" : `${count}회 가져옴`, + openLatestImportedTask: "최근에 가져온 작업 열기", + openLatestImportedTaskFor: name => `${name}에서 가져온 최신 작업을 엽니다.`, + import: "수입", + importAgain: "다시 가져오기", + importTask: name => `${name} 가져오기`, + importTaskAgain: name => `${name} 다시 가져오기`, + importing: "가져오는 중…", + importingTask: name => `${name} 가져오기`, + importInProgressTitle: "가져오기 진행 중", + importInProgressDescription: name => `"${name}"을(를) 가져오는 중입니다. Maka는 작업이 도착하자마자 작업을 엽니다.`, + importFailedTitle: "가져오기 실패", + importFailedFallback: "이 대화는 변환하거나 저장할 수 없습니다. 소스를 확인하고 다시 시도해 보세요.", + importRecoveredTitle: "수입확인됨", + importRecoveredDescription: name => `이제 가져온 작업을 "${name}"에 사용할 수 있습니다.`, + importNotRecordedTitle: "새 할 일을 찾을 수 없습니다.", + importNotRecordedDescription: "새 작업이 기록되지 않았으므로 다시 시도해도 안전합니다.", + importOutcomeUnknownTitle: "가져오기 결과 확인", + selectAllAriaLabel: "모두 선택하거나 선택하지 않음", + selectedCount: (selected, listed) => `${selected} / ${listed} 선택됨`, + selectRowAriaLabel: name => `${name}을 선택하세요`, + importSelected: "선택 항목 가져오기", + batchProgress: (done, total) => `${done} / ${total} 가져오기`, + batchDoneTitle: imported => `${imported} 대화를 가져왔습니다.`, + batchDuplicated: count => `그 중 ${count}은 이전에 수입되었으며 현재 두 번 존재합니다.`, + batchFailed: count => `${count} 더 이상 가져올 수 없습니다.`, + batchNothingImported: "가져온 대화가 없습니다.", + importOutcomeUnknownDescription: names => `Maka는 다음 수입의 결과를 확인할 수 없었습니다: ${names.map(name => `“${name}”`).join(', ')}. 먼저 작업 목록을 살펴보고 이미 있는 항목을 다시 가져오지 마세요.` +} } satisfies UiCatalog; export function getExternalSessionImportCopy(locale: UiLocale): ExternalSessionImportCopy { diff --git a/apps/desktop/src/renderer/locales/mcp-copy.ts b/apps/desktop/src/renderer/locales/mcp-copy.ts index 3d6c41275a..36fa3fd0a3 100644 --- a/apps/desktop/src/renderer/locales/mcp-copy.ts +++ b/apps/desktop/src/renderer/locales/mcp-copy.ts @@ -189,6 +189,152 @@ const MCP_COPY = { stdioProtocolHelp: 'Auto-negotiate and “2026-07-28 only” first start a short-lived probe with the same command, arguments, working directory, and environment. The session process starts only after the probe exits. Existing configurations default to Legacy and start one process.', }, }, + ko: { + errors: { + load: "MCP를 로드하지 못했습니다.", + install: name => `${name}을(를) 설치하지 못했습니다.`, + cancelInstall: name => `${name} 설치를 취소하지 못했습니다.`, + save: "MCP를 저장하지 못했습니다.", + import: "MCP를 가져오지 못했습니다.", + update: "MCP를 업데이트하지 못했습니다.", + test: "MCP 테스트 실패", + remove: "MCP를 삭제하지 못했습니다.", + unavailableStatus: "서버가 사용 가능 상태를 반환하지 않았습니다.", + mapLine: line => `${line} 행에서는 KEY=값을 사용해야 합니다.`, + importJson: "MCP 구성은 유효한 JSON이어야 합니다.", + importObject: "MCP JSON은 객체여야 합니다.", + importVersion: version => `지원되지 않는 MCP 구성 버전 ${version}; 버전 1, 2, 3이 지원됩니다.`, + importServersObject: "mcpServers는 객체여야 합니다.", + importProtocolVersion: "원격 프로토콜 기본 설정에는 버전 2 또는 3이 필요합니다. stdio 프로토콜 기본 설정에는 버전 3이 필요합니다." + }, + toast: { + templateInstalled: name => `${name} 템플릿이 설치되었습니다.`, + templateInstalledDetail: "연결을 활성화하기 전에 설치됨에서 자격 증명 구성을 완료하세요.", + installed: name => `${name} 설치됨`, + installedDetail: "발견된 도구는 다음 에이전트 턴부터 적용됩니다.", + installCancelled: name => `${name} 설치 취소됨`, + saved: "MCP가 저장되었습니다.", + savedDetail: "새로운 도구는 다음 에이전트 차례부터 적용됩니다.", + imported: "MCP를 가져왔습니다.", + importedDetail: count => `${count} ${count === 1 ? '서버' : '서버'}를 가져왔습니다.`, + connectionOk: "MCP 연결이 정상입니다.", + toolLatency: (count, latencyMs) => `${count}개 도구 · ${latencyMs}ms`, + connectionFailed: "MCP 연결 실패", + removed: "MCP가 삭제되었습니다." + }, + remove: { + title: id => `MCP “${id}”을 삭제하시겠습니까?`, + description: "해당 도구는 다음 에이전트 차례부터 제거되며 구성은 자동으로 복원될 수 없습니다.", + confirm: "삭제", + cancel: "취소" + }, + page: { + actionsAria: "MCP 작업", + refreshing: "상쾌하다…", + refresh: "새로 고치다", + add: "MCP 추가", + metaInstalled: count => `${count} 설치됨`, + metaErrors: count => `${count} ${count === 1 ? "연결 오류" : "연결 오류"}`, + searchMatches: count => `${count}개 일치`, + workspaceAria: "MCP 마켓플레이스 및 설치된 서버", + toolbarAria: "MCP 브라우저 컨트롤", + setupTitle: "Maka를 작업 환경에 연결하세요", + setupDescription: "선별된 템플릿으로 시작하거나 stdio, Streamable HTTP 또는 SSE 서버를 추가하세요.", + localStdio: "로컬 스튜디오", + categoriesAria: "MCP 카테고리", + market: "시장", + installed: "설치됨", + searchPlaceholder: "MCP 검색…", + searchAria: "MCP 검색", + noMarket: "일치하는 MCP 서버가 없습니다.", + noMarketDetail: query => `다른 키워드를 시도하거나 "${query}"을 지워 모든 템플릿을 보십시오.`, + clearSearch: "검색 지우기", + loading: "MCP 구성을 읽는 중…", + noInstalled: "MCP 서버가 설치되지 않았습니다.", + noInstalledDetail: "마켓플레이스에서 템플릿을 선택하거나 자체 서버를 수동으로 추가하세요.", + browseMarket: "마켓플레이스 찾아보기", + noInstalledMatch: "일치하는 설치된 MCP 서버가 없습니다.", + noInstalledMatchDetail: query => `다른 키워드를 사용하거나 "${query}"을 지워 설치된 모든 서버를 확인하세요.` + }, + detail: { + label: "서버 세부정보", + enabled: "활성화됨", + transport: "수송", + endpoint: "엔드포인트", + toolsLabel: "도구", + statusLabel: "상태", + protocolLabel: "MCP 프로토콜", + negotiatedProtocol: (era, revision) => `${era === 'modern' ? "현대의" : "유산"} · ${revision}`, + inspectorOpened: id => `${id} 세부정보 열림` + }, + card: { + macOnly: "macOS만 해당", + manage: "관리하다", + cancellingAria: name => `${name} 설치 취소 중`, + cancelAria: name => `${name} 설치 취소`, + installAria: name => `${name} 설치`, + cancelling: "취소 중…", + cancel: "설치 취소", + install: "설치하다" + }, + row: { + testing: "테스트 중…", + test: "시험", + edit: "편집하다", + delete: "삭제", + tools: count => `${count}개 도구`, + disabled: "장애가 있는", + disconnected: "연결이 끊김", + connecting: "연결 중", + connected: count => `${count}개 도구`, + failed: "연결 실패" + }, + editor: { + importTitle: "JSON에서 가져오기", + editTitle: id => `${id} 편집`, + addTitle: "MCP 추가", + importSubtitle: "mcpServers 구성을 붙여넣습니다. 이름이 일치하는 서버가 업데이트됩니다.", + manualSubtitle: "구성은 현재 작업 영역에 대한 mcp.json에 저장됩니다.", + modeAria: "MCP 추가 방법", + manual: "수동 구성", + pasteJson: "JSON 붙여넣기", + jsonConfig: "JSON 구성", + jsonHelp: "완전한 mcpServers 구성 또는 서버 맵을 지원합니다. 이 가져오기에서 생략된 기존 MCP 서버는 보존됩니다.", + cancel: "취소", + importConnect: "가져오기 및 연결", + transportAria: "연결 방법", + localStdio: "로컬 스튜디오", + remoteUrl: "원격 URL", + serverId: "서버ID", + command: "명령", + commandPlaceholder: "npx -y @modelcontextprotocol/서버 파일 시스템 /경로/to/폴더", + commandHelp: "전체 명령줄; 공백이 포함된 인수를 인용합니다. 쉘에서는 해석되지 않습니다.", + workingDirectory: "작업 디렉토리", + workingDirectoryPlaceholder: "선택사항(예: /path/to/project)", + environment: "환경", + environmentHelp: "한 줄에 하나의 KEY=값 항목; 이 MCP에 필요한 변수를 완성합니다.", + url: 'MCP URL', + headers: "HTTP 헤더", + headersHelp: "한 줄에 하나의 헤더=값 항목이 있습니다.", + saveConnect: "저장하고 연결하세요", + required: "이 필드는 필수입니다.", + invalidUrl: "유효한 HTTP 또는 HTTPS URL을 입력하세요.", + unbalancedQuote: "닫히지 않은 견적.", + transportLabel: "수송", + transportAuto: "자동 대체", + transportStreamableHttp: "스트리밍 가능한 HTTP", + transportLegacySse: "레거시 SSE", + protocolLabel: "프로토콜 선호도", + protocolLegacy: "유산", + protocolAuto: "자동 협상", + protocolModern: "2026년 7월 28일에만", + protocolHelp: "기존 구성은 기본적으로 레거시로 설정됩니다. 자동 협상은 서버 응답에서 시대를 선택합니다.", + sseProtocolHelp: "레거시 SSE는 레거시 프로토콜 시대만 지원합니다.", + expandAdvanced: "고급 설정 표시", + collapseAdvanced: "고급 설정 숨기기", + stdioProtocolHelp: "자동 협상 및 \"2026-07-28만\"은 먼저 동일한 명령, 인수, 작업 디렉터리 및 환경을 사용하여 단기 프로브를 시작합니다. 세션 프로세스는 프로브가 종료된 후에만 시작됩니다. 기존 구성은 기본적으로 레거시로 설정되고 하나의 프로세스를 시작합니다." + } +} } satisfies UiCatalog; export function getMcpCopy(locale: UiLocale): McpCopy { diff --git a/apps/desktop/src/renderer/locales/onboarding-copy.ts b/apps/desktop/src/renderer/locales/onboarding-copy.ts index e68a6243d4..f795a16b14 100644 --- a/apps/desktop/src/renderer/locales/onboarding-copy.ts +++ b/apps/desktop/src/renderer/locales/onboarding-copy.ts @@ -149,6 +149,47 @@ const ONBOARDING_COPY_BY_LOCALE: UiCatalog = { skip: 'Skip onboarding', snapshotErrorFallback: 'First-run status is temporarily unavailable. Try again later.', }, + ko: { + hero: { + needs_connection: { + eyebrow: 'Maka에 오신 것을 환영합니다', + title: 'AI를 연결하고 첫 작업을 시작하세요.', + body: 'Maka는 로컬에서 실행되며 사용자의 모델 계정 또는 API 키를 사용합니다. 계속하려면 공급자를 선택하세요.', + cta: { label: '모든 공급자 보기' }, + }, + needs_connection_credentials: { + eyebrow: '연결 설정 계속', + title: '이 연결에 자격 증명을 추가하세요.', + body: 'Maka가 모델을 호출할 수 있도록 API 키를 추가하거나 계정 로그인을 완료하세요.', + cta: { label: '자격 증명 구성' }, + }, + needs_model: { + eyebrow: '연결 설정 계속', + title: '이 연결에 사용할 모델을 활성화하세요.', + body: '대화 가능한 모델을 활성화하면 첫 작업을 시작할 수 있습니다.', + cta: { label: '사용 가능한 모델 선택' }, + }, + 'blocked:all_connections_unhealthy': { + eyebrow: '연결 확인 필요', + title: '모델 연결을 일시적으로 사용할 수 없습니다.', + body: '확인을 통과한 연결이 없습니다. 자격 증명, 로그인 상태 또는 네트워크를 확인한 후 다시 테스트하세요.', + cta: { label: '모델 연결 수정' }, + tone: 'destructive', + }, + 'blocked:all_connections_retired': { + eyebrow: '연결 확인 필요', + title: '연결에서 사용하는 로그인 방식이 중단되었습니다.', + body: '이 연결에서 사용하는 로그인 방식이 Maka에서 제거되어 더 이상 로그인하거나 대화에 사용할 수 없습니다. 계속하려면 새 모델 연결을 추가하세요.', + cta: { label: '모델 연결 추가' }, + tone: 'destructive', + }, + }, + needsConnection: { pickLabel: '공급자 선택', browseProviders: '모든 공급자 보기' }, + refresh: { connection: '연결 다시 확인', credentials: '자격 증명 다시 확인', model: '모델 다시 확인', blocked: '연결 다시 확인' }, + connectionLabel: '연결', + skip: '온보딩 건너뛰기', + snapshotErrorFallback: '첫 실행 상태를 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.', + }, }; export function getOnboardingCopy(locale: UiLocale): OnboardingCatalog { diff --git a/apps/desktop/src/renderer/locales/permission-center-copy.ts b/apps/desktop/src/renderer/locales/permission-center-copy.ts index 3a2e4713ea..ec83771290 100644 --- a/apps/desktop/src/renderer/locales/permission-center-copy.ts +++ b/apps/desktop/src/renderer/locales/permission-center-copy.ts @@ -191,6 +191,53 @@ const PERMISSION_CENTER_COPY = { auditSection: 'Audit records', noAudit: 'No audit records', auditAria: (label) => `${label} audit records`, impact: 'Affects', opening: 'Opening…', openSettings: 'Open System Settings', requesting: 'Requesting…', request: 'Request permission', dragGrant: 'Guide me', dragGranting: 'Opening…', }, + ko: { + readiness: { + not_configured: { label: 'Needs setup', detail: 'Enable the feature or complete its configuration first.', tone: 'neutral' }, + denied: { label: 'Denied by system', detail: 'A required system permission was denied or is unsupported on this platform.', tone: 'error' }, + enabled: { label: 'Available', detail: 'The current snapshot is available; see the layers below for details.', tone: 'success' }, + degraded: { label: 'Partially available', detail: 'Some functionality is available, but runtime, permission, or sub-feature work remains.', tone: 'attention' }, + paused: { label: 'Paused', detail: 'The feature was explicitly disabled while its configuration remains saved.', tone: 'neutral' }, + }, + osPermissions: { + accessibility: { label: 'Accessibility', purpose: 'Computer Use needs it to read window focus and simulate keyboard or mouse input.', impact: 'Computer Use · automated keyboard and mouse input' }, + screen_recording: { label: 'Screen Recording', purpose: 'Computer Use needs it to read window contents; future screen activity recording will use it too.', impact: 'Computer Use · screenshot context' }, + notifications: { label: 'Notifications', purpose: 'System alerts use it for permission requests and completed reviews.', impact: 'Permission alerts · Daily Review completion' }, + automation: { label: 'Automation (Apple Events)', purpose: 'Computer Use needs per-target authorization to control other apps.', impact: 'Computer Use · cross-app automation' }, + }, + osStates: { + unsupported: { label: 'Unsupported on this platform', tone: 'neutral' }, unknown: { label: 'Status unavailable', tone: 'neutral' }, + not_determined: { label: 'Waiting for permission', tone: 'attention' }, denied: { label: 'Denied', tone: 'error' }, granted: { label: 'Granted', tone: 'success' }, + }, + loading: 'Loading permission snapshot', readFailed: 'Could not read permission snapshot', noData: 'The permission service returned no data.', readAgain: 'Read again', + actionFailed: 'Permission action failed', + actionFailures: { + invalid_id: 'Internal error: the permission ID was not recognized.', + unsupported_platform: 'This operating system does not support the permission action.', + unsupported_permission: 'This platform does not provide a direct entry point for the permission.', + denied: 'Permission was not granted. You can enable it in System Settings.', + already_open: 'Another permission guide is still open. Finish or close it first.', + open_settings_failed: 'Could not open System Settings. Open Privacy & Security manually.', + failed: 'The permission action did not succeed. Try again later.', + }, + title: 'Permissions and capabilities', subtitle: 'Review the system permissions Maka needs and their current state. Open the matching Privacy & Security section directly to grant or revoke access.', + lastRead: 'Last read: ', detectAgain: 'Check again', summaryAria: 'Filter system permissions by authorization status', summaryFilterAria: (label, count, selected) => selected ? `${label}, ${count}; filter selected. Press again to show all permissions` : `Show only ${label.toLowerCase()} permissions, ${count}`, granted: 'Granted', pending: 'Waiting', denied: 'Denied', other: 'Unknown / unsupported', + osSection: 'System permissions', osSectionHelp: 'OS-level permission states reported to Maka. Use the action on the right to open the matching Privacy & Security section in System Settings.', osListAria: 'System permission list', + capabilitiesSection: 'Feature capabilities', capabilitiesHelp: 'Each readiness state combines the feature toggle, configuration, system permissions, and runtime probe.', + capabilityListAria: 'Feature capability list', + footnote: 'Maka never grants Accessibility, Automation, or Screen Recording automatically. High-risk automation must remain individually approved, auditable, and revocable. This page only reads the current snapshot; permission changes still happen in System Settings under Privacy & Security.', + layers: { + aria: (label) => `${label} capability state details`, feature: 'Feature toggle', configuration: 'Configuration', approval: 'Action approval', memory: 'Memory writes', runtime: 'Runtime probe', + featureStates: { enabled: 'Enabled', partial: 'Partially available', disabled: 'Disabled', not_available: 'Unavailable' }, + configurationStates: { not_required: 'No configuration needed', missing: 'Configuration required', present: 'Configured' }, + approvalStates: { not_required: 'No approval needed', required_per_action: 'Approval required for every call', required_scoped_lease: 'Authorized by target and action category', pending: 'Approval pending', approved: 'Approved for this task', denied: 'Denied for this task' }, + memoryStates: { not_applicable: 'No memory writes', disabled: 'Memory writes disabled', draft_required: 'Draft a memory protocol first', accepted: 'Memory writes accepted' }, + runtimeStates: { not_available: 'No runtime probe available', not_run: 'Probe not run', healthy: 'Probe passed', degraded: 'Probe degraded' }, + }, + requiredPermissions: 'Required system permissions', requiredPermissionsAria: (label) => `${label} required system permissions`, guidance: 'Suggested actions', guidanceAria: (label) => `${label} suggested actions`, + auditSection: 'Audit records', noAudit: 'No audit records', auditAria: (label) => `${label} audit records`, + impact: 'Affects', opening: 'Opening…', openSettings: 'Open System Settings', requesting: 'Requesting…', request: 'Request permission', dragGrant: 'Guide me', dragGranting: 'Opening…', + } } satisfies UiCatalog; export function getPermissionCenterCopy(locale: UiLocale): PermissionCenterCopy { diff --git a/apps/desktop/src/renderer/locales/plan-mode-copy.ts b/apps/desktop/src/renderer/locales/plan-mode-copy.ts index df164c8bee..75400a3714 100644 --- a/apps/desktop/src/renderer/locales/plan-mode-copy.ts +++ b/apps/desktop/src/renderer/locales/plan-mode-copy.ts @@ -89,6 +89,72 @@ const COPY = { stepStatuses: { pending: 'Not started', in_progress: 'In progress', completed: 'Completed', skipped: 'Skipped' }, }, }, + ko: { + abandonConfirmation: { + title: '이 계획을 포기하시겠습니까?', + description: title => '“' + title + '”의 실행 기록은 유지되지만 다시 시작할 수 없습니다.', + confirm: '계획 포기', + cancel: '취소' + }, + proposal: { + ...{ + aria: "계획 제안", + kicker: "계획 제안", + revision: "개정", + steps: "단계", + risks: "위험", + revise: "변경 요청", + execute: "계획 실행", + statuses: { + pending_approval: "승인을 기다리는 중", + approved: "승인됨", + stale: "구식" + } + }, + aria: '계획 제안', + kicker: '계획 제안', + revision: '수정본', + steps: '단계', + risks: '위험', + revise: '변경 요청', + execute: '계획 실행', + statuses: { + pending_approval: '승인 대기 중', + approved: '승인됨', + stale: '오래됨' + } + }, + execution: { + ...{ + aria: "계획 실행 현황", + interrupted: "계획이 중단되었습니다", + running: "계획 실행", + approvedPlan: "승인된 계획", + stepCount: (completed, total) => `${completed}/${total} ${total === 1 ? 'step' : 'steps'}`, + resume: "재개하다", + abandon: "계획 포기", + stepStatuses: { + pending: "시작되지 않음", + in_progress: "진행중", + completed: "완전한", + skipped: "건너뛰었습니다." + } + }, + aria: '계획 실행 상태', + interrupted: '계획 중단됨', + running: '계획 실행 중', + approvedPlan: '승인된 계획', + stepCount: (completed, total) => completed + '/' + total + '단계', + resume: '실행 재개', + abandon: '계획 포기', + stepStatuses: { + pending: '시작되지 않음', + in_progress: '진행 중', + completed: '완료', + skipped: '건너뜀' + } + } +} } satisfies UiCatalog; export function getPlanModeCopy(locale: UiLocale): PlanModeCopy { diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts index 43f65b3d0c..7ba6447bde 100644 --- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts +++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts @@ -214,7 +214,7 @@ const EN = { pendingTurnRequestCount: (count: number) => `${count} pending Turn ${count === 1 ? 'request' : 'requests'}`, } satisfies SessionCollaborationCopy; -const COPY = { zh: ZH, en: EN } satisfies UiCatalog; +const COPY = { zh: ZH, en: EN, ko: EN } satisfies UiCatalog; export function getSessionCollaborationCopy(locale: UiLocale) { return COPY[locale]; diff --git a/apps/desktop/src/renderer/locales/settings-bot-copy.ts b/apps/desktop/src/renderer/locales/settings-bot-copy.ts index 698aa1f4d9..f4447e4638 100644 --- a/apps/desktop/src/renderer/locales/settings-bot-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-bot-copy.ts @@ -19,132 +19,923 @@ import type { StatusSemantic } from '@maka/ui'; import type { BotProvider, BotReadinessState } from '@maka/core/bot-chat-settings'; - import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - -type WidenCopy = T extends string - ? string - : T extends (...args: infer Args) => string - ? (...args: Args) => string - : { [K in keyof T]: K extends 'tone' ? T[K] : WidenCopy }; - +type WidenCopy = T extends string ? string : T extends ((...args: infer Args) => string) ? (...args: Args) => string : { [K in keyof T]: K extends 'tone' ? T[K] : WidenCopy }; const zhCopy = { providers: { - telegram: { label: 'Telegram', help: '通过 @BotFather 创建 Bot 并获取 Token' }, - feishu: { label: '飞书', help: '在飞书开放平台创建应用并获取凭证' }, - wecom: { label: '企业微信', help: '通过企业微信 AI 应用接入,使用 WebSocket 长连接' }, - wechat: { label: '微信', help: '通过本机 wechat-bridge 接入个人微信,需 iOS / Android 微信 8.0.70+。' }, - discord: { label: 'Discord', help: '在 Discord Developer Portal 创建 Bot' }, - dingtalk: { label: '钉钉', help: '在钉钉开发者后台创建机器人应用' }, - qq: { label: 'QQ', help: '在 QQ 开放平台创建机器人并获取 AppID 和 AppSecret' }, - slack: { label: 'Slack', help: '使用 Bot Token 与 App-Level Token 通过 Socket Mode 接入' }, - } satisfies Record, + telegram: { + label: 'Telegram', + help: '通过 @BotFather 创建 Bot 并获取 Token' + }, + feishu: { + label: '飞书', + help: '在飞书开放平台创建应用并获取凭证' + }, + wecom: { + label: '企业微信', + help: '通过企业微信 AI 应用接入,使用 WebSocket 长连接' + }, + wechat: { + label: '微信', + help: '通过本机 wechat-bridge 接入个人微信,需 iOS / Android 微信 8.0.70+。' + }, + discord: { + label: 'Discord', + help: '在 Discord Developer Portal 创建 Bot' + }, + dingtalk: { + label: '钉钉', + help: '在钉钉开发者后台创建机器人应用' + }, + qq: { + label: 'QQ', + help: '在 QQ 开放平台创建机器人并获取 AppID 和 AppSecret' + }, + slack: { + label: 'Slack', + help: '使用 Bot Token 与 App-Level Token 通过 Socket Mode 接入' + } + } satisfies Record, readiness: { - unscaffolded: { label: '未开放', detail: '该平台当前不可作为远程接入渠道。', tone: 'neutral' }, - scaffolded: { label: '待配置', detail: '等待补齐这个平台需要的凭据配置。', tone: 'neutral' }, - configured: { label: '已配置', detail: '已填写配置;等待完成凭据或运行态验证。', tone: 'attention' }, - credentials_valid: { label: '凭据有效', detail: '凭据探测通过;这不代表已能收发消息。', tone: 'attention' }, - operational: { label: '运行可用', detail: '最近一次真实运行探测成功。', tone: 'success' }, - degraded: { label: '运行降级', detail: '之前可用,但最近运行态探测失败。', tone: 'error' }, - } satisfies Record, - planned: { label: '未开放', detail: '该平台当前不会保存为远程接入渠道或定时任务投递目标。', tone: 'neutral' as const }, + unscaffolded: { + label: '未开放', + detail: '该平台当前不可作为远程接入渠道。', + tone: 'neutral' + }, + scaffolded: { + label: '待配置', + detail: '等待补齐这个平台需要的凭据配置。', + tone: 'neutral' + }, + configured: { + label: '已配置', + detail: '已填写配置;等待完成凭据或运行态验证。', + tone: 'attention' + }, + credentials_valid: { + label: '凭据有效', + detail: '凭据探测通过;这不代表已能收发消息。', + tone: 'attention' + }, + operational: { + label: '运行可用', + detail: '最近一次真实运行探测成功。', + tone: 'success' + }, + degraded: { + label: '运行降级', + detail: '之前可用,但最近运行态探测失败。', + tone: 'error' + } + } satisfies Record, + planned: { + label: '未开放', + detail: '该平台当前不会保存为远程接入渠道或定时任务投递目标。', + tone: 'neutral' as const + }, status: { - disabled: '开关关闭', noToken: '等待填写 Bot Token', missingFeishuCredentials: '等待填写飞书 App ID 或 App Secret', - feishuDomainRequired: '飞书凭据有效,等待填写事件订阅域名', feishuEventsNotConnected: '飞书凭据有效,等待事件回调接入', - unavailable: '该平台当前不可作为远程接入渠道', stopped: '监听已停止', detailsInLogs: '运行态详情请见日志', - polling: '长轮询', gateway: '事件通道', webhook: 'Webhook', none: '无', + disabled: '开关关闭', + noToken: '等待填写 Bot Token', + missingFeishuCredentials: '等待填写飞书 App ID 或 App Secret', + feishuDomainRequired: '飞书凭据有效,等待填写事件订阅域名', + feishuEventsNotConnected: '飞书凭据有效,等待事件回调接入', + unavailable: '该平台当前不可作为远程接入渠道', + stopped: '监听已停止', + detailsInLogs: '运行态详情请见日志', + polling: '长轮询', + gateway: '事件通道', + webhook: 'Webhook', + none: '无' }, overview: { - loadFailed: '远程接入状态载入失败', reload: '重新载入', active: '正在使用', sortHint: '按需要处理、最近活动排序', - empty: '还没有正在使用的渠道', emptyHelp: '从下方选择一个消息平台开始配置。', more: '接入更多渠道', choose: '选择平台开始配置', - listening: '监听中', manageAria: (name: string, status: string) => `管理 ${name},${status}`, connectAria: (name: string) => `接入 ${name}`, + loadFailed: '远程接入状态载入失败', + reload: '重新载入', + active: '正在使用', + sortHint: '按需要处理、最近活动排序', + empty: '还没有正在使用的渠道', + emptyHelp: '从下方选择一个消息平台开始配置。', + more: '接入更多渠道', + choose: '选择平台开始配置', + listening: '监听中', + manageAria: (name: string, status: string) => `管理 ${name},${status}`, + connectAria: (name: string) => `接入 ${name}` }, page: { - saveFailed: (name: string) => `${name} 保存失败`, loadFailed: '载入远程接入状态失败', refreshFailed: '刷新远程接入状态失败', - credentialVerified: (name: string) => `${name} 凭据已验证`, credentialVerifiedDetail: '凭据检查已通过。', credentialTestFailed: (name: string) => `${name} 凭据测试失败`, credentialTestFailedDetail: '请检查凭据和网络设置后重试。', testError: (name: string) => `${name} 测试出错`, - listening: (name: string) => `${name} 已开始监听`, notListening: (name: string) => `${name} 启动后未进入监听`, startFailed: (name: string) => `${name} 启动失败`, - disconnectTitle: '断开微信登录?', disconnectDescription: '将清除本机保存的扫码登录凭据,之后需要重新扫码才能继续使用微信渠道。', - disconnect: '断开登录', cancel: '取消', disconnected: '微信登录已断开', credentialsCleared: '本机关联凭据已清除。', + saveFailed: (name: string) => `${name} 保存失败`, + loadFailed: '载入远程接入状态失败', + refreshFailed: '刷新远程接入状态失败', + credentialVerified: (name: string) => `${name} 凭据已验证`, + credentialVerifiedDetail: '凭据检查已通过。', + credentialTestFailed: (name: string) => `${name} 凭据测试失败`, + credentialTestFailedDetail: '请检查凭据和网络设置后重试。', + testError: (name: string) => `${name} 测试出错`, + listening: (name: string) => `${name} 已开始监听`, + notListening: (name: string) => `${name} 启动后未进入监听`, + startFailed: (name: string) => `${name} 启动失败`, + disconnectTitle: '断开微信登录?', + disconnectDescription: '将清除本机保存的扫码登录凭据,之后需要重新扫码才能继续使用微信渠道。', + disconnect: '断开登录', + cancel: '取消', + disconnected: '微信登录已断开', + credentialsCleared: '本机关联凭据已清除。' }, detail: { - unavailableHint: '该平台未开放,暂不能启用。', scanFirstHint: '先扫码接入后才能启用。', testFirstHint: '先测试并连接后才能启用。', - back: '返回远程接入', configDocs: '查看配置文档', enableAria: (name: string) => `启用${name}渠道`, listening: '正在监听新消息', healthy: '连接正常,无需处理。', - actionsAria: (name: string) => `${name}渠道操作`, quickBind: '快捷绑定', scanLogin: '扫码登录', scanConnect: '扫码接入', disconnecting: '断开中…', disconnectWechat: '断开微信登录', bridgeQr: '本机桥接二维码', - testing: '测试中…', test: '测试连接', connecting: '连接中…', testAndConnect: '测试并连接', restarting: '重启中…', restart: '重启监听', - runtimeAria: (name: string) => `${name}运行状态`, identity: '身份', unknownIdentity: '未获取', connectionType: '通道类型', lastEvent: '最近事件', noneYet: '暂无', lastTest: '最近一次测试', neverTested: '从未测试', - statusRefreshFailed: '运行状态刷新失败', latestFailure: '最近一次失败', latestFailureDetail: '请检查配置、网络和运行日志后重试。', savedButNotConnected: '凭据已保存,但连接尚未成功启动。', setupMethod: '接入方式', connectionSettings: '连接配置', localCredentials: '凭据仅保存在本机', autosave: '自动保存', - setupAria: (name: string) => `${name}接入方式`, quickRecommended: '快捷接入(推荐)', manual: '手动配置', quickAria: (name: string) => `${name}快捷接入`, - quickWecomTitle: '扫码创建并绑定机器人', quickTitle: '扫码自动创建应用与机器人', quickWecomDetail: '企业管理员扫码确认后,Maka 会保存 Bot ID 与 Secret 并启动长连接。', - quickQqTitle: '使用手机 QQ 扫码创建并绑定机器人', quickQqDetail: '确认后,QQ 会安全返回 AppID 与 AppSecret,Maka 在本机保存凭据并启动 Gateway。', + unavailableHint: '该平台未开放,暂不能启用。', + scanFirstHint: '先扫码接入后才能启用。', + testFirstHint: '先测试并连接后才能启用。', + back: '返回远程接入', + configDocs: '查看配置文档', + enableAria: (name: string) => `启用${name}渠道`, + listening: '正在监听新消息', + healthy: '连接正常,无需处理。', + actionsAria: (name: string) => `${name}渠道操作`, + quickBind: '快捷绑定', + scanLogin: '扫码登录', + scanConnect: '扫码接入', + disconnecting: '断开中…', + disconnectWechat: '断开微信登录', + bridgeQr: '本机桥接二维码', + testing: '测试中…', + test: '测试连接', + connecting: '连接中…', + testAndConnect: '测试并连接', + restarting: '重启中…', + restart: '重启监听', + runtimeAria: (name: string) => `${name}运行状态`, + identity: '身份', + unknownIdentity: '未获取', + connectionType: '通道类型', + lastEvent: '最近事件', + noneYet: '暂无', + lastTest: '最近一次测试', + neverTested: '从未测试', + statusRefreshFailed: '运行状态刷新失败', + latestFailure: '最近一次失败', + latestFailureDetail: '请检查配置、网络和运行日志后重试。', + savedButNotConnected: '凭据已保存,但连接尚未成功启动。', + setupMethod: '接入方式', + connectionSettings: '连接配置', + localCredentials: '凭据仅保存在本机', + autosave: '自动保存', + setupAria: (name: string) => `${name}接入方式`, + quickRecommended: '快捷接入(推荐)', + manual: '手动配置', + quickAria: (name: string) => `${name}快捷接入`, + quickWecomTitle: '扫码创建并绑定机器人', + quickTitle: '扫码自动创建应用与机器人', + quickWecomDetail: '企业管理员扫码确认后,Maka 会保存 Bot ID 与 Secret 并启动长连接。', + quickQqTitle: '使用手机 QQ 扫码创建并绑定机器人', + quickQqDetail: '确认后,QQ 会安全返回 AppID 与 AppSecret,Maka 在本机保存凭据并启动 Gateway。', telegramOfficialFlow: 'Telegram 官方目前仅支持通过 @BotFather 获取 Bot Token,不提供扫码创建 Bot 并回传 Token 的 API。', - quickDetail: '扫码确认后,Maka 会在 main process 内保存凭据并启动消息连接。', feishuRegionAria: '选择飞书账号区域', feishu: '飞书', - beginQuickBind: '开始快捷绑定', scanWith: (name: string) => `使用${name}扫码接入`, planned: '这个平台当前只作为平台清单展示,不会进入可用渠道,也不会保存为定时任务投递目标。', - credentialsSaved: (name: string) => `${name}凭据已保存`, scanComplete: (name: string) => `${name}已完成扫码接入`, savedAndConnected: '凭据已安全保存并开始连接', - proxy: '代理地址', chinaRequired: '(国内网络必填)', authOnly: '(仅用于 Bot 鉴权)', telegramProxyAria: 'Telegram 代理地址', - telegramNotice: '请打开网络的 TUN 模式后重启应用,以便完成 Telegram Bot 设置', feishuCredentialId: '飞书凭据 ID', feishuSecret: '飞书 App Secret', - feishuDomain: '飞书域名', feishuOption: '飞书 (feishu.cn)', discordProxyAria: 'Discord 代理地址', + quickDetail: '扫码确认后,Maka 会在 main process 内保存凭据并启动消息连接。', + feishuRegionAria: '选择飞书账号区域', + feishu: '飞书', + beginQuickBind: '开始快捷绑定', + scanWith: (name: string) => `使用${name}扫码接入`, + planned: '这个平台当前只作为平台清单展示,不会进入可用渠道,也不会保存为定时任务投递目标。', + credentialsSaved: (name: string) => `${name}凭据已保存`, + scanComplete: (name: string) => `${name}已完成扫码接入`, + savedAndConnected: '凭据已安全保存并开始连接', + proxy: '代理地址', + chinaRequired: '(国内网络必填)', + authOnly: '(仅用于 Bot 鉴权)', + telegramProxyAria: 'Telegram 代理地址', + telegramNotice: '请打开网络的 TUN 模式后重启应用,以便完成 Telegram Bot 设置', + feishuCredentialId: '飞书凭据 ID', + feishuSecret: '飞书 App Secret', + feishuDomain: '飞书域名', + feishuOption: '飞书 (feishu.cn)', + discordProxyAria: 'Discord 代理地址', discordNotice: '国内网络访问 Discord:上方代理仅作用于 Bot 鉴权请求,消息收发走 WebSocket 长连接需要系统级代理。请打开网络的 TUN 模式后重启应用。', - dingtalkId: '钉钉应用密钥', dingtalkSecret: '钉钉 Client Secret', wecomBotPlaceholder: '企业微信 AI 应用 Bot ID', wecomBotAria: '企业微信 Bot ID', - wecomSecretPlaceholder: 'AI 应用 Secret', wecomSecretAria: '企业微信 Secret', qqId: 'QQ 应用编号', - allowedUsersLabel: (count: number, max: number) => `允许的用户 ID(${count} / ${max})`, allowedUsersPlaceholder: '每行一个用户 ID,留空表示不限\n例如:123456789', + dingtalkId: '钉钉应用密钥', + dingtalkSecret: '钉钉 Client Secret', + wecomBotPlaceholder: '企业微信 AI 应用 Bot ID', + wecomBotAria: '企业微信 Bot ID', + wecomSecretPlaceholder: 'AI 应用 Secret', + wecomSecretAria: '企业微信 Secret', + qqId: 'QQ 应用编号', + allowedUsersLabel: (count: number, max: number) => `允许的用户 ID(${count} / ${max})`, + allowedUsersPlaceholder: '每行一个用户 ID,留空表示不限\n例如:123456789', allowedUsersHelp: 'Telegram 用户 ID 是 64 位整数;填入后只接收列表里这些 ID 的来信,其它人发的消息会被静默忽略(不会回弹任何提示)。', - limitReached: '(已达到上限)', invalidUsers: (values: string) => `下列不是数字 ID,可能是用户名之类的输入,匹配不到任何人:${values}`, moreInvalid: (count: number) => ` 等 ${count} 项`, + limitReached: '(已达到上限)', + invalidUsers: (values: string) => `下列不是数字 ID,可能是用户名之类的输入,匹配不到任何人:${values}`, + moreInvalid: (count: number) => ` 等 ${count} 项` }, onboarding: { providers: { - dingtalk: { title: '配置钉钉', ariaLabel: '配置钉钉扫码接入', qrAlt: '配置钉钉二维码', subtitle: '在钉钉中扫码完成应用注册', waiting: '请使用钉钉扫描二维码并确认授权', scanned: '已扫码,请在钉钉中完成确认' }, - feishu: { title: '配置飞书', ariaLabel: '配置飞书扫码接入', qrAlt: '配置飞书二维码', subtitle: '使用飞书扫描二维码,自动创建并配置机器人', waiting: '请使用飞书扫描二维码并确认创建', scanned: '已扫码,请在飞书中完成确认' }, - wecom: { title: '配置企业微信', ariaLabel: '配置企业微信扫码接入', qrAlt: '配置企业微信二维码', subtitle: '快捷绑定会自动创建并连接企业微信机器人', waiting: '打开企业微信,扫描二维码完成机器人创建', scanned: '已扫码,请在企业微信中完成确认' }, - wechat: { title: '扫码登录', ariaLabel: '微信扫码登录', qrAlt: '微信扫码登录二维码', subtitle: '请使用微信扫描二维码完成连接', waiting: '请使用微信扫描二维码并在手机上确认', scanned: '已扫码,请在微信中完成确认' }, - qq: { title: '配置 QQ', ariaLabel: '配置 QQ 扫码接入', qrAlt: '配置 QQ 二维码', subtitle: '使用手机 QQ 扫码创建并绑定机器人', waiting: '请使用手机 QQ 扫描二维码并确认绑定', scanned: '已扫码,请在 QQ 中完成确认' }, - }, - lark: { title: '配置 Lark', ariaLabel: '配置 Lark 扫码接入', qrAlt: '配置 Lark 二维码', subtitle: '使用 Lark 扫描二维码,自动创建并配置机器人', waiting: '请使用 Lark 扫描二维码并确认创建', scanned: '已扫码,请在 Lark 中完成确认' }, - connectedRefreshFailed: (message: string) => `连接已完成,但状态刷新失败:${message}`, close: (title: string) => `关闭${title}`, - generatingAria: '正在生成二维码', privacy: '凭据仅保存在本机,不会传给 renderer 或 Maka 云端。', openBrowser: '无法扫码?在浏览器中打开', - done: '完成', regenerate: '重新生成', refreshQr: '刷新二维码', cancel: '取消', generating: '正在生成安全二维码…', connecting: '授权完成,正在保存凭据并启动连接…', - connected: (name: string) => `${name} 已连接`, connectedWarning: '凭据已保存,但连接尚未成功启动。', expired: '二维码已过期,请重新生成', denied: '授权已取消,请重新生成二维码', cancelled: '扫码接入已取消', failed: '扫码接入失败,请重试', preparing: '准备扫码接入…', + dingtalk: { + title: '配置钉钉', + ariaLabel: '配置钉钉扫码接入', + qrAlt: '配置钉钉二维码', + subtitle: '在钉钉中扫码完成应用注册', + waiting: '请使用钉钉扫描二维码并确认授权', + scanned: '已扫码,请在钉钉中完成确认' + }, + feishu: { + title: '配置飞书', + ariaLabel: '配置飞书扫码接入', + qrAlt: '配置飞书二维码', + subtitle: '使用飞书扫描二维码,自动创建并配置机器人', + waiting: '请使用飞书扫描二维码并确认创建', + scanned: '已扫码,请在飞书中完成确认' + }, + wecom: { + title: '配置企业微信', + ariaLabel: '配置企业微信扫码接入', + qrAlt: '配置企业微信二维码', + subtitle: '快捷绑定会自动创建并连接企业微信机器人', + waiting: '打开企业微信,扫描二维码完成机器人创建', + scanned: '已扫码,请在企业微信中完成确认' + }, + wechat: { + title: '扫码登录', + ariaLabel: '微信扫码登录', + qrAlt: '微信扫码登录二维码', + subtitle: '请使用微信扫描二维码完成连接', + waiting: '请使用微信扫描二维码并在手机上确认', + scanned: '已扫码,请在微信中完成确认' + }, + qq: { + title: '配置 QQ', + ariaLabel: '配置 QQ 扫码接入', + qrAlt: '配置 QQ 二维码', + subtitle: '使用手机 QQ 扫码创建并绑定机器人', + waiting: '请使用手机 QQ 扫描二维码并确认绑定', + scanned: '已扫码,请在 QQ 中完成确认' + } + }, + lark: { + title: '配置 Lark', + ariaLabel: '配置 Lark 扫码接入', + qrAlt: '配置 Lark 二维码', + subtitle: '使用 Lark 扫描二维码,自动创建并配置机器人', + waiting: '请使用 Lark 扫描二维码并确认创建', + scanned: '已扫码,请在 Lark 中完成确认' + }, + connectedRefreshFailed: (message: string) => `连接已完成,但状态刷新失败:${message}`, + close: (title: string) => `关闭${title}`, + generatingAria: '正在生成二维码', + privacy: '凭据仅保存在本机,不会传给 renderer 或 Maka 云端。', + openBrowser: '无法扫码?在浏览器中打开', + done: '完成', + regenerate: '重新生成', + refreshQr: '刷新二维码', + cancel: '取消', + generating: '正在生成安全二维码…', + connecting: '授权完成,正在保存凭据并启动连接…', + connected: (name: string) => `${name} 已连接`, + connectedWarning: '凭据已保存,但连接尚未成功启动。', + expired: '二维码已过期,请重新生成', + denied: '授权已取消,请重新生成二维码', + cancelled: '扫码接入已取消', + failed: '扫码接入失败,请重试', + preparing: '准备扫码接入…' }, wechat: { - token: '微信 Bot Token', tokenPlaceholder: '本机 wechat-bridge Bearer Token', collapseAdvanced: '收起高级设置', expandAdvanced: '高级设置(公众号 / 本机 bridge 地址)', - bridgeAddress: '本机 bridge 地址', appId: '公众号 App ID', appIdPlaceholder: '微信公众号 App ID', - appSecret: '公众号 App Secret', appSecretPlaceholder: '微信公众号 App Secret', + token: '微信 Bot Token', + tokenPlaceholder: '本机 wechat-bridge Bearer Token', + collapseAdvanced: '收起高级设置', + expandAdvanced: '高级设置(公众号 / 本机 bridge 地址)', + bridgeAddress: '本机 bridge 地址', + appId: '公众号 App ID', + appIdPlaceholder: '微信公众号 App ID', + appSecret: '公众号 App Secret', + appSecretPlaceholder: '微信公众号 App Secret', advancedNotice: '本机 bridge 默认为 http://127.0.0.1:18400。公众号 App ID / App Secret 仅用于公众号消息发送,个人微信扫码登录走本机 bridge。', - readQrFailed: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。', title: '微信扫码登录', subtitle: '使用手机微信扫描二维码,并在手机上确认登录本机 wechat-bridge。', close: '关闭微信扫码登录', - generating: '正在生成二维码…', loggedIn: '微信已登录,返回后可以测试连接或重启监听。', expired: '二维码已过期', expiredHint: '刷新二维码后重新扫码即可继续登录。', refreshing: '刷新中…', refresh: '刷新二维码', qrAlt: '微信扫码登录二维码', - waiting: '等待扫码确认… 窗口会每 3 秒刷新登录状态。', retrying: '重试中…', retry: '重试', bridgeGenerating: 'bridge 正在生成二维码', bridgeGeneratingHint: '二维码就绪后会自动显示,也可以手动重新获取。', fetching: '获取中…', fetchAgain: '重新获取', - }, + readQrFailed: '读取本机 wechat-bridge 二维码失败,请确认 bridge 已启动。', + title: '微信扫码登录', + subtitle: '使用手机微信扫描二维码,并在手机上确认登录本机 wechat-bridge。', + close: '关闭微信扫码登录', + generating: '正在生成二维码…', + loggedIn: '微信已登录,返回后可以测试连接或重启监听。', + expired: '二维码已过期', + expiredHint: '刷新二维码后重新扫码即可继续登录。', + refreshing: '刷新中…', + refresh: '刷新二维码', + qrAlt: '微信扫码登录二维码', + waiting: '等待扫码确认… 窗口会每 3 秒刷新登录状态。', + retrying: '重试中…', + retry: '重试', + bridgeGenerating: 'bridge 正在生成二维码', + bridgeGeneratingHint: '二维码就绪后会自动显示,也可以手动重新获取。', + fetching: '获取中…', + fetchAgain: '重新获取' + } } as const; - export type BotSettingsCopy = WidenCopy; - const enCopy: BotSettingsCopy = { providers: { - telegram: { label: 'Telegram', help: 'Create a bot with @BotFather and get its token' }, feishu: { label: 'Feishu', help: 'Create an app and credentials in Feishu Open Platform' }, - wecom: { label: 'WeCom', help: 'Connect a WeCom AI app over a persistent WebSocket' }, wechat: { label: 'WeChat', help: 'Connect personal WeChat through the local bridge; requires WeChat 8.0.70+ on iOS or Android.' }, - discord: { label: 'Discord', help: 'Create a bot in Discord Developer Portal' }, dingtalk: { label: 'DingTalk', help: 'Create a bot app in DingTalk Developer Console' }, qq: { label: 'QQ', help: 'Create a bot in QQ Open Platform and get its AppID and AppSecret' }, - slack: { label: 'Slack', help: 'Connect with a Bot Token and App-Level Token over Socket Mode' }, + telegram: { + label: 'Telegram', + help: 'Create a bot with @BotFather and get its token' + }, + feishu: { + label: 'Feishu', + help: 'Create an app and credentials in Feishu Open Platform' + }, + wecom: { + label: 'WeCom', + help: 'Connect a WeCom AI app over a persistent WebSocket' + }, + wechat: { + label: 'WeChat', + help: 'Connect personal WeChat through the local bridge; requires WeChat 8.0.70+ on iOS or Android.' + }, + discord: { + label: 'Discord', + help: 'Create a bot in Discord Developer Portal' + }, + dingtalk: { + label: 'DingTalk', + help: 'Create a bot app in DingTalk Developer Console' + }, + qq: { + label: 'QQ', + help: 'Create a bot in QQ Open Platform and get its AppID and AppSecret' + }, + slack: { + label: 'Slack', + help: 'Connect with a Bot Token and App-Level Token over Socket Mode' + } }, readiness: { - unscaffolded: { label: 'Unavailable', detail: 'This platform cannot currently be used for remote access.', tone: 'neutral' }, scaffolded: { label: 'Setup required', detail: 'Add the credentials required by this platform.', tone: 'neutral' }, - configured: { label: 'Configured', detail: 'Configuration is saved; credential or runtime validation is still required.', tone: 'attention' }, credentials_valid: { label: 'Credentials valid', detail: 'The credential check passed; this does not prove messages can be sent or received.', tone: 'attention' }, - operational: { label: 'Operational', detail: 'The latest live runtime check succeeded.', tone: 'success' }, degraded: { label: 'Degraded', detail: 'This channel worked before, but the latest runtime check failed.', tone: 'error' }, + unscaffolded: { + label: 'Unavailable', + detail: 'This platform cannot currently be used for remote access.', + tone: 'neutral' + }, + scaffolded: { + label: 'Setup required', + detail: 'Add the credentials required by this platform.', + tone: 'neutral' + }, + configured: { + label: 'Configured', + detail: 'Configuration is saved; credential or runtime validation is still required.', + tone: 'attention' + }, + credentials_valid: { + label: 'Credentials valid', + detail: 'The credential check passed; this does not prove messages can be sent or received.', + tone: 'attention' + }, + operational: { + label: 'Operational', + detail: 'The latest live runtime check succeeded.', + tone: 'success' + }, + degraded: { + label: 'Degraded', + detail: 'This channel worked before, but the latest runtime check failed.', + tone: 'error' + } + }, + planned: { + label: 'Unavailable', + detail: 'This platform is not saved as a remote-access channel or scheduled-task delivery target.', + tone: 'neutral' + }, + status: { + disabled: 'Turned off', + noToken: 'Waiting for Bot Token', + missingFeishuCredentials: 'Waiting for Feishu App ID or App Secret', + feishuDomainRequired: 'Feishu credentials are valid; add the event subscription domain', + feishuEventsNotConnected: 'Feishu credentials are valid; connect the event callback', + unavailable: 'This platform cannot currently be used for remote access', + stopped: 'Listener stopped', + detailsInLogs: 'See logs for runtime details', + polling: 'Long polling', + gateway: 'Event channel', + webhook: 'Webhook', + none: 'None' + }, + overview: { + loadFailed: 'Failed to load remote-access status', + reload: 'Reload', + active: 'In use', + sortHint: 'Sorted by attention needed and recent activity', + empty: 'No channels are in use', + emptyHelp: 'Choose a messaging platform below to begin setup.', + more: 'Connect more channels', + choose: 'Choose a platform to begin setup', + listening: 'Listening', + manageAria: (name, status) => `Manage ${name}, ${status}`, + connectAria: name => `Connect ${name}` + }, + page: { + saveFailed: name => `Failed to save ${name}`, + loadFailed: 'Failed to load remote-access status', + refreshFailed: 'Failed to refresh remote-access status', + credentialVerified: name => `${name} credentials verified`, + credentialVerifiedDetail: 'The credential check passed.', + credentialTestFailed: name => `${name} credential test failed`, + credentialTestFailedDetail: 'Check the credentials and network settings, then try again.', + testError: name => `${name} test error`, + listening: name => `${name} is listening`, + notListening: name => `${name} did not start listening`, + startFailed: name => `Failed to start ${name}`, + disconnectTitle: 'Disconnect WeChat?', + disconnectDescription: 'This clears the saved local QR sign-in credentials. You will need to scan again to keep using WeChat.', + disconnect: 'Disconnect', + cancel: 'Cancel', + disconnected: 'WeChat disconnected', + credentialsCleared: 'Local linked-session credentials cleared.' }, - planned: { label: 'Unavailable', detail: 'This platform is not saved as a remote-access channel or scheduled-task delivery target.', tone: 'neutral' }, - status: { disabled: 'Turned off', noToken: 'Waiting for Bot Token', missingFeishuCredentials: 'Waiting for Feishu App ID or App Secret', feishuDomainRequired: 'Feishu credentials are valid; add the event subscription domain', feishuEventsNotConnected: 'Feishu credentials are valid; connect the event callback', unavailable: 'This platform cannot currently be used for remote access', stopped: 'Listener stopped', detailsInLogs: 'See logs for runtime details', polling: 'Long polling', gateway: 'Event channel', webhook: 'Webhook', none: 'None' }, - overview: { loadFailed: 'Failed to load remote-access status', reload: 'Reload', active: 'In use', sortHint: 'Sorted by attention needed and recent activity', empty: 'No channels are in use', emptyHelp: 'Choose a messaging platform below to begin setup.', more: 'Connect more channels', choose: 'Choose a platform to begin setup', listening: 'Listening', manageAria: (name, status) => `Manage ${name}, ${status}`, connectAria: (name) => `Connect ${name}` }, - page: { saveFailed: (name) => `Failed to save ${name}`, loadFailed: 'Failed to load remote-access status', refreshFailed: 'Failed to refresh remote-access status', credentialVerified: (name) => `${name} credentials verified`, credentialVerifiedDetail: 'The credential check passed.', credentialTestFailed: (name) => `${name} credential test failed`, credentialTestFailedDetail: 'Check the credentials and network settings, then try again.', testError: (name) => `${name} test error`, listening: (name) => `${name} is listening`, notListening: (name) => `${name} did not start listening`, startFailed: (name) => `Failed to start ${name}`, disconnectTitle: 'Disconnect WeChat?', disconnectDescription: 'This clears the saved local QR sign-in credentials. You will need to scan again to keep using WeChat.', disconnect: 'Disconnect', cancel: 'Cancel', disconnected: 'WeChat disconnected', credentialsCleared: 'Local linked-session credentials cleared.' }, detail: { - unavailableHint: 'This platform is not available and cannot be enabled.', scanFirstHint: 'Scan to connect before enabling this channel.', testFirstHint: 'Test and connect before enabling this channel.', back: 'Back to Remote access', configDocs: 'View setup guide', enableAria: (name) => `Enable ${name} channel`, listening: 'Listening for new messages', healthy: 'Connection healthy. No action needed.', actionsAria: (name) => `${name} channel actions`, quickBind: 'Quick connect', scanLogin: 'Scan to sign in', scanConnect: 'Scan to connect', disconnecting: 'Disconnecting…', disconnectWechat: 'Disconnect WeChat', bridgeQr: 'Local bridge QR code', testing: 'Testing…', test: 'Test connection', connecting: 'Connecting…', testAndConnect: 'Test and connect', restarting: 'Restarting…', restart: 'Restart listener', runtimeAria: (name) => `${name} runtime status`, identity: 'Identity', unknownIdentity: 'Unavailable', connectionType: 'Connection type', lastEvent: 'Last event', noneYet: 'None', lastTest: 'Last test', neverTested: 'Never tested', statusRefreshFailed: 'Failed to refresh runtime status', latestFailure: 'Latest failure', latestFailureDetail: 'Check the configuration, network, and runtime logs, then try again.', savedButNotConnected: 'Credentials were saved, but the connection did not start.', setupMethod: 'Connection method', connectionSettings: 'Connection settings', localCredentials: 'Credentials stay on this device', autosave: 'Saved automatically', setupAria: (name) => `${name} connection method`, quickRecommended: 'Quick setup (recommended)', manual: 'Manual setup', quickAria: (name) => `${name} quick setup`, quickWecomTitle: 'Scan to create and connect a bot', quickTitle: 'Scan to create an app and bot', quickWecomDetail: 'After an administrator confirms the scan, Maka saves the Bot ID and Secret and starts the persistent connection.', quickQqTitle: 'Scan with mobile QQ to create and bind a bot', quickQqDetail: 'After confirmation, QQ securely returns the AppID and AppSecret; Maka stores them locally and starts the Gateway.', telegramOfficialFlow: 'Telegram officially requires a Bot Token from @BotFather and does not provide an API that creates a bot by QR scan and returns its token.', quickDetail: 'After confirmation, Maka stores credentials in the main process and starts the message connection.', feishuRegionAria: 'Choose Feishu account region', feishu: 'Feishu', beginQuickBind: 'Start quick connect', scanWith: (name) => `Scan with ${name}`, planned: 'This platform is shown in the catalog only. It will not become an active channel or a scheduled-task delivery target.', credentialsSaved: (name) => `${name} credentials saved`, scanComplete: (name) => `${name} QR setup complete`, savedAndConnected: 'Credentials saved securely and connection started', proxy: 'Proxy URL', chinaRequired: '(required on networks in mainland China)', authOnly: '(Bot authentication only)', telegramProxyAria: 'Telegram proxy URL', telegramNotice: 'Enable TUN mode in your network tool and restart the app to complete Telegram Bot setup.', feishuCredentialId: 'Feishu credential ID', feishuSecret: 'Feishu App Secret', feishuDomain: 'Feishu domain', feishuOption: 'Feishu (feishu.cn)', discordProxyAria: 'Discord proxy URL', discordNotice: 'For Discord access from mainland China, the proxy above covers Bot authentication only. Message WebSockets require a system-level proxy. Enable TUN mode and restart the app.', dingtalkId: 'DingTalk app key', dingtalkSecret: 'DingTalk Client Secret', wecomBotPlaceholder: 'WeCom AI app Bot ID', wecomBotAria: 'WeCom Bot ID', wecomSecretPlaceholder: 'AI app Secret', wecomSecretAria: 'WeCom Secret', qqId: 'QQ app ID', allowedUsersLabel: (count, max) => `Allowed user IDs (${count} / ${max})`, allowedUsersPlaceholder: 'One user ID per line; leave empty to allow everyone\nExample: 123456789', allowedUsersHelp: 'Telegram user IDs are 64-bit integers. When set, only messages from these IDs are accepted; all others are silently ignored.', limitReached: '(limit reached)', invalidUsers: (values) => `These entries are not numeric IDs and may be usernames, so they will not match anyone: ${values}`, moreInvalid: (count) => ` and ${count} more`, + unavailableHint: 'This platform is not available and cannot be enabled.', + scanFirstHint: 'Scan to connect before enabling this channel.', + testFirstHint: 'Test and connect before enabling this channel.', + back: 'Back to Remote access', + configDocs: 'View setup guide', + enableAria: name => `Enable ${name} channel`, + listening: 'Listening for new messages', + healthy: 'Connection healthy. No action needed.', + actionsAria: name => `${name} channel actions`, + quickBind: 'Quick connect', + scanLogin: 'Scan to sign in', + scanConnect: 'Scan to connect', + disconnecting: 'Disconnecting…', + disconnectWechat: 'Disconnect WeChat', + bridgeQr: 'Local bridge QR code', + testing: 'Testing…', + test: 'Test connection', + connecting: 'Connecting…', + testAndConnect: 'Test and connect', + restarting: 'Restarting…', + restart: 'Restart listener', + runtimeAria: name => `${name} runtime status`, + identity: 'Identity', + unknownIdentity: 'Unavailable', + connectionType: 'Connection type', + lastEvent: 'Last event', + noneYet: 'None', + lastTest: 'Last test', + neverTested: 'Never tested', + statusRefreshFailed: 'Failed to refresh runtime status', + latestFailure: 'Latest failure', + latestFailureDetail: 'Check the configuration, network, and runtime logs, then try again.', + savedButNotConnected: 'Credentials were saved, but the connection did not start.', + setupMethod: 'Connection method', + connectionSettings: 'Connection settings', + localCredentials: 'Credentials stay on this device', + autosave: 'Saved automatically', + setupAria: name => `${name} connection method`, + quickRecommended: 'Quick setup (recommended)', + manual: 'Manual setup', + quickAria: name => `${name} quick setup`, + quickWecomTitle: 'Scan to create and connect a bot', + quickTitle: 'Scan to create an app and bot', + quickWecomDetail: 'After an administrator confirms the scan, Maka saves the Bot ID and Secret and starts the persistent connection.', + quickQqTitle: 'Scan with mobile QQ to create and bind a bot', + quickQqDetail: 'After confirmation, QQ securely returns the AppID and AppSecret; Maka stores them locally and starts the Gateway.', + telegramOfficialFlow: 'Telegram officially requires a Bot Token from @BotFather and does not provide an API that creates a bot by QR scan and returns its token.', + quickDetail: 'After confirmation, Maka stores credentials in the main process and starts the message connection.', + feishuRegionAria: 'Choose Feishu account region', + feishu: 'Feishu', + beginQuickBind: 'Start quick connect', + scanWith: name => `Scan with ${name}`, + planned: 'This platform is shown in the catalog only. It will not become an active channel or a scheduled-task delivery target.', + credentialsSaved: name => `${name} credentials saved`, + scanComplete: name => `${name} QR setup complete`, + savedAndConnected: 'Credentials saved securely and connection started', + proxy: 'Proxy URL', + chinaRequired: '(required on networks in mainland China)', + authOnly: '(Bot authentication only)', + telegramProxyAria: 'Telegram proxy URL', + telegramNotice: 'Enable TUN mode in your network tool and restart the app to complete Telegram Bot setup.', + feishuCredentialId: 'Feishu credential ID', + feishuSecret: 'Feishu App Secret', + feishuDomain: 'Feishu domain', + feishuOption: 'Feishu (feishu.cn)', + discordProxyAria: 'Discord proxy URL', + discordNotice: 'For Discord access from mainland China, the proxy above covers Bot authentication only. Message WebSockets require a system-level proxy. Enable TUN mode and restart the app.', + dingtalkId: 'DingTalk app key', + dingtalkSecret: 'DingTalk Client Secret', + wecomBotPlaceholder: 'WeCom AI app Bot ID', + wecomBotAria: 'WeCom Bot ID', + wecomSecretPlaceholder: 'AI app Secret', + wecomSecretAria: 'WeCom Secret', + qqId: 'QQ app ID', + allowedUsersLabel: (count, max) => `Allowed user IDs (${count} / ${max})`, + allowedUsersPlaceholder: 'One user ID per line; leave empty to allow everyone\nExample: 123456789', + allowedUsersHelp: 'Telegram user IDs are 64-bit integers. When set, only messages from these IDs are accepted; all others are silently ignored.', + limitReached: '(limit reached)', + invalidUsers: values => `These entries are not numeric IDs and may be usernames, so they will not match anyone: ${values}`, + moreInvalid: count => ` and ${count} more` }, onboarding: { - providers: { dingtalk: { title: 'Set up DingTalk', ariaLabel: 'Set up DingTalk with a QR code', qrAlt: 'DingTalk setup QR code', subtitle: 'Scan in DingTalk to register the app', waiting: 'Scan with DingTalk and confirm authorization', scanned: 'Scanned. Complete confirmation in DingTalk.' }, feishu: { title: 'Set up Feishu', ariaLabel: 'Set up Feishu with a QR code', qrAlt: 'Feishu setup QR code', subtitle: 'Scan with Feishu to create and configure the bot', waiting: 'Scan with Feishu and confirm creation', scanned: 'Scanned. Complete confirmation in Feishu.' }, wecom: { title: 'Set up WeCom', ariaLabel: 'Set up WeCom with a QR code', qrAlt: 'WeCom setup QR code', subtitle: 'Quick setup creates and connects a WeCom bot', waiting: 'Open WeCom and scan to create the bot', scanned: 'Scanned. Complete confirmation in WeCom.' }, wechat: { title: 'Scan to sign in', ariaLabel: 'WeChat QR sign-in', qrAlt: 'WeChat sign-in QR code', subtitle: 'Scan with WeChat to connect', waiting: 'Scan with WeChat and confirm on your phone', scanned: 'Scanned. Complete confirmation in WeChat.' }, qq: { title: 'Set up QQ', ariaLabel: 'Set up QQ with a QR code', qrAlt: 'QQ setup QR code', subtitle: 'Scan with mobile QQ to create and bind a bot', waiting: 'Scan with mobile QQ and confirm binding', scanned: 'Scanned. Complete confirmation in QQ.' } }, - lark: { title: 'Set up Lark', ariaLabel: 'Set up Lark with a QR code', qrAlt: 'Lark setup QR code', subtitle: 'Scan with Lark to create and configure the bot', waiting: 'Scan with Lark and confirm creation', scanned: 'Scanned. Complete confirmation in Lark.' }, connectedRefreshFailed: (message) => `Connected, but status refresh failed: ${message}`, close: (title) => `Close ${title}`, generatingAria: 'Generating QR code', privacy: 'Credentials stay on this device and are never sent to the renderer or Maka cloud.', openBrowser: 'Cannot scan? Open in browser', done: 'Done', regenerate: 'Generate again', refreshQr: 'Refresh QR code', cancel: 'Cancel', generating: 'Generating a secure QR code…', connecting: 'Authorization complete. Saving credentials and starting connection…', connected: (name) => `${name} connected`, connectedWarning: 'Credentials were saved, but the connection did not start.', expired: 'QR code expired. Generate a new one.', denied: 'Authorization cancelled. Generate a new QR code.', cancelled: 'QR setup cancelled', failed: 'QR setup failed. Try again.', preparing: 'Preparing QR setup…', + providers: { + dingtalk: { + title: 'Set up DingTalk', + ariaLabel: 'Set up DingTalk with a QR code', + qrAlt: 'DingTalk setup QR code', + subtitle: 'Scan in DingTalk to register the app', + waiting: 'Scan with DingTalk and confirm authorization', + scanned: 'Scanned. Complete confirmation in DingTalk.' + }, + feishu: { + title: 'Set up Feishu', + ariaLabel: 'Set up Feishu with a QR code', + qrAlt: 'Feishu setup QR code', + subtitle: 'Scan with Feishu to create and configure the bot', + waiting: 'Scan with Feishu and confirm creation', + scanned: 'Scanned. Complete confirmation in Feishu.' + }, + wecom: { + title: 'Set up WeCom', + ariaLabel: 'Set up WeCom with a QR code', + qrAlt: 'WeCom setup QR code', + subtitle: 'Quick setup creates and connects a WeCom bot', + waiting: 'Open WeCom and scan to create the bot', + scanned: 'Scanned. Complete confirmation in WeCom.' + }, + wechat: { + title: 'Scan to sign in', + ariaLabel: 'WeChat QR sign-in', + qrAlt: 'WeChat sign-in QR code', + subtitle: 'Scan with WeChat to connect', + waiting: 'Scan with WeChat and confirm on your phone', + scanned: 'Scanned. Complete confirmation in WeChat.' + }, + qq: { + title: 'Set up QQ', + ariaLabel: 'Set up QQ with a QR code', + qrAlt: 'QQ setup QR code', + subtitle: 'Scan with mobile QQ to create and bind a bot', + waiting: 'Scan with mobile QQ and confirm binding', + scanned: 'Scanned. Complete confirmation in QQ.' + } + }, + lark: { + title: 'Set up Lark', + ariaLabel: 'Set up Lark with a QR code', + qrAlt: 'Lark setup QR code', + subtitle: 'Scan with Lark to create and configure the bot', + waiting: 'Scan with Lark and confirm creation', + scanned: 'Scanned. Complete confirmation in Lark.' + }, + connectedRefreshFailed: message => `Connected, but status refresh failed: ${message}`, + close: title => `Close ${title}`, + generatingAria: 'Generating QR code', + privacy: 'Credentials stay on this device and are never sent to the renderer or Maka cloud.', + openBrowser: 'Cannot scan? Open in browser', + done: 'Done', + regenerate: 'Generate again', + refreshQr: 'Refresh QR code', + cancel: 'Cancel', + generating: 'Generating a secure QR code…', + connecting: 'Authorization complete. Saving credentials and starting connection…', + connected: name => `${name} connected`, + connectedWarning: 'Credentials were saved, but the connection did not start.', + expired: 'QR code expired. Generate a new one.', + denied: 'Authorization cancelled. Generate a new QR code.', + cancelled: 'QR setup cancelled', + failed: 'QR setup failed. Try again.', + preparing: 'Preparing QR setup…' }, - wechat: { token: 'WeChat Bot Token', tokenPlaceholder: 'Local wechat-bridge Bearer Token', collapseAdvanced: 'Hide advanced settings', expandAdvanced: 'Advanced settings (Official Account / local bridge URL)', bridgeAddress: 'Local bridge URL', appId: 'Official Account App ID', appIdPlaceholder: 'WeChat Official Account App ID', appSecret: 'Official Account App Secret', appSecretPlaceholder: 'WeChat Official Account App Secret', advancedNotice: 'The local bridge defaults to http://127.0.0.1:18400. Official Account App ID and App Secret are used only for Official Account messaging; personal WeChat QR sign-in uses the local bridge.', readQrFailed: 'Could not read a QR code from the local wechat-bridge. Make sure the bridge is running.', title: 'WeChat QR sign-in', subtitle: 'Scan the QR code with WeChat and confirm signing in to the local wechat-bridge on your phone.', close: 'Close WeChat QR sign-in', generating: 'Generating QR code…', loggedIn: 'WeChat is signed in. Return to test the connection or restart the listener.', expired: 'QR code expired', expiredHint: 'Refresh the QR code and scan again to continue signing in.', refreshing: 'Refreshing…', refresh: 'Refresh QR code', qrAlt: 'WeChat sign-in QR code', waiting: 'Waiting for confirmation… Sign-in status refreshes every 3 seconds.', retrying: 'Retrying…', retry: 'Retry', bridgeGenerating: 'The bridge is generating a QR code', bridgeGeneratingHint: 'The QR code appears automatically once ready; you can also fetch it again.', fetching: 'Fetching…', fetchAgain: 'Fetch again' }, + wechat: { + token: 'WeChat Bot Token', + tokenPlaceholder: 'Local wechat-bridge Bearer Token', + collapseAdvanced: 'Hide advanced settings', + expandAdvanced: 'Advanced settings (Official Account / local bridge URL)', + bridgeAddress: 'Local bridge URL', + appId: 'Official Account App ID', + appIdPlaceholder: 'WeChat Official Account App ID', + appSecret: 'Official Account App Secret', + appSecretPlaceholder: 'WeChat Official Account App Secret', + advancedNotice: 'The local bridge defaults to http://127.0.0.1:18400. Official Account App ID and App Secret are used only for Official Account messaging; personal WeChat QR sign-in uses the local bridge.', + readQrFailed: 'Could not read a QR code from the local wechat-bridge. Make sure the bridge is running.', + title: 'WeChat QR sign-in', + subtitle: 'Scan the QR code with WeChat and confirm signing in to the local wechat-bridge on your phone.', + close: 'Close WeChat QR sign-in', + generating: 'Generating QR code…', + loggedIn: 'WeChat is signed in. Return to test the connection or restart the listener.', + expired: 'QR code expired', + expiredHint: 'Refresh the QR code and scan again to continue signing in.', + refreshing: 'Refreshing…', + refresh: 'Refresh QR code', + qrAlt: 'WeChat sign-in QR code', + waiting: 'Waiting for confirmation… Sign-in status refreshes every 3 seconds.', + retrying: 'Retrying…', + retry: 'Retry', + bridgeGenerating: 'The bridge is generating a QR code', + bridgeGeneratingHint: 'The QR code appears automatically once ready; you can also fetch it again.', + fetching: 'Fetching…', + fetchAgain: 'Fetch again' + } }; - -const BOT_SETTINGS_COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog; - +const BOT_SETTINGS_COPY = { + zh: zhCopy, + en: enCopy, + ko: { + providers: { + telegram: { + label: "전보", + help: "@BotFather로 봇을 만들고 토큰을 받으세요." + }, + feishu: { + label: "페이슈", + help: "Feishu Open Platform에서 앱 및 자격 증명 만들기" + }, + wecom: { + label: "위컴", + help: "영구 WebSocket을 통해 WeCom AI 앱 연결" + }, + wechat: { + label: "위챗", + help: "로컬 브리지를 통해 개인 WeChat을 연결합니다. iOS 또는 Android에는 WeChat 8.0.70 이상이 필요합니다." + }, + discord: { + label: "불화", + help: "Discord 개발자 포털에서 봇 만들기" + }, + dingtalk: { + label: "딩톡", + help: "DingTalk 개발자 콘솔에서 봇 앱 만들기" + }, + qq: { + label: 'QQ', + help: "QQ Open Platform에서 봇을 생성하고 AppID와 AppSecret을 받으세요" + }, + slack: { + label: "느슨하게", + help: "소켓 모드를 통해 봇 토큰 및 앱 수준 토큰과 연결" + } + }, + readiness: { + unscaffolded: { + label: "없는", + detail: "이 플랫폼은 현재 원격 액세스에 사용할 수 없습니다.", + tone: 'neutral' + }, + scaffolded: { + label: "설정 필요", + detail: "이 플랫폼에 필요한 자격 증명을 추가합니다.", + tone: 'neutral' + }, + configured: { + label: "구성됨", + detail: "구성이 저장되었습니다. 자격 증명 또는 런타임 유효성 검사는 여전히 필요합니다.", + tone: 'attention' + }, + credentials_valid: { + label: "유효한 자격 증명", + detail: "자격 증명 확인을 통과했습니다. 이는 메시지를 보내거나 받을 수 있다는 것을 증명하지 않습니다.", + tone: 'attention' + }, + operational: { + label: "운영", + detail: "최신 라이브 런타임 확인이 성공했습니다.", + tone: 'success' + }, + degraded: { + label: "타락한", + detail: "이 채널은 이전에는 작동했지만 최신 런타임 확인에 실패했습니다.", + tone: 'error' + } + }, + planned: { + label: "없는", + detail: "이 플랫폼은 원격 액세스 채널이나 예약된 작업 전달 대상으로 저장되지 않습니다.", + tone: 'neutral' + }, + status: { + disabled: "꺼짐", + noToken: "봇 토큰을 기다리는 중", + missingFeishuCredentials: "Feishu 앱 ID 또는 앱 비밀을 기다리는 중", + feishuDomainRequired: "Feishu 자격 증명이 유효합니다. 이벤트 구독 도메인 추가", + feishuEventsNotConnected: "Feishu 자격 증명이 유효합니다. 이벤트 콜백 연결", + unavailable: "이 플랫폼은 현재 원격 액세스에 사용할 수 없습니다.", + stopped: "리스너가 중지되었습니다.", + detailsInLogs: "런타임 세부정보는 로그를 참조하세요.", + polling: "긴 폴링", + gateway: "이벤트 채널", + webhook: "웹훅", + none: "없음" + }, + overview: { + loadFailed: "원격 액세스 상태를 로드하지 못했습니다.", + reload: "새로고침", + active: "사용 중", + sortHint: "주의가 필요한 항목과 최근 활동을 기준으로 정렬됨", + empty: "사용 중인 채널이 없습니다.", + emptyHelp: "설정을 시작하려면 아래에서 메시지 플랫폼을 선택하세요.", + more: "더 많은 채널을 연결하세요", + choose: "설정을 시작하려면 플랫폼을 선택하세요", + listening: "청취", + manageAria: (name, status) => `${name}, ${status} 관리`, + connectAria: name => `${name} 연결` + }, + page: { + saveFailed: name => `${name}을(를) 저장하지 못했습니다.`, + loadFailed: "원격 액세스 상태를 로드하지 못했습니다.", + refreshFailed: "원격 액세스 상태를 새로 고치지 못했습니다.", + credentialVerified: name => `${name} 자격 증명이 확인되었습니다.`, + credentialVerifiedDetail: "자격 증명 확인이 통과되었습니다.", + credentialTestFailed: name => `${name} 자격 증명 테스트에 실패했습니다.`, + credentialTestFailedDetail: "자격 증명과 네트워크 설정을 확인한 후 다시 시도하세요.", + testError: name => `${name} 테스트 오류`, + listening: name => `${name}이 듣고 있습니다`, + notListening: name => `${name}이 듣기 시작하지 않았습니다`, + startFailed: name => `${name}을(를) 시작하지 못했습니다.`, + disconnectTitle: "WeChat을 연결 해제하시겠습니까?", + disconnectDescription: "이렇게 하면 저장된 로컬 QR 로그인 자격 증명이 지워집니다. WeChat을 계속 사용하려면 다시 스캔해야 합니다.", + disconnect: "연결 끊기", + cancel: "취소", + disconnected: "위챗 연결이 끊어졌습니다", + credentialsCleared: "로컬 연결 세션 자격 증명이 지워졌습니다." + }, + detail: { + unavailableHint: "이 플랫폼은 사용할 수 없으며 활성화할 수 없습니다.", + scanFirstHint: "이 채널을 활성화하기 전에 연결하려면 스캔하세요.", + testFirstHint: "이 채널을 활성화하기 전에 테스트하고 연결하세요.", + back: "원격 액세스로 돌아가기", + configDocs: "설정 가이드 보기", + enableAria: name => `${name} 채널 활성화`, + listening: "새 메시지 듣기", + healthy: "연결 상태가 양호합니다. 조치가 필요하지 않습니다.", + actionsAria: name => `${name} 채널 작업`, + quickBind: "빠른 연결", + scanLogin: "스캔하여 로그인", + scanConnect: "스캔하여 연결", + disconnecting: "연결 해제 중…", + disconnectWechat: "위챗 연결 끊기", + bridgeQr: "지역 교량 QR 코드", + testing: "테스트 중…", + test: "테스트 연결", + connecting: "연결 중…", + testAndConnect: "테스트 및 연결", + restarting: "다시 시작하는 중…", + restart: "리스너 다시 시작", + runtimeAria: name => `${name} 런타임 상태`, + identity: "신원", + unknownIdentity: "없는", + connectionType: "연결 유형", + lastEvent: "마지막 이벤트", + noneYet: "없음", + lastTest: "마지막 테스트", + neverTested: "테스트한 적 없음", + statusRefreshFailed: "런타임 상태를 새로 고치지 못했습니다.", + latestFailure: "최근 실패", + latestFailureDetail: "구성, 네트워크, 런타임 로그를 확인한 후 다시 시도하세요.", + savedButNotConnected: "자격 증명이 저장되었지만 연결이 시작되지 않았습니다.", + setupMethod: "연결 방법", + connectionSettings: "연결 설정", + localCredentials: "사용자 인증 정보는 이 기기에 유지됩니다.", + autosave: "자동으로 저장됨", + setupAria: name => `${name} 연결 방법`, + quickRecommended: "빠른 설정(권장)", + manual: "수동 설정", + quickAria: name => `${name} 빠른 설정`, + quickWecomTitle: "스캔하여 봇 생성 및 연결", + quickTitle: "스캔하여 앱과 봇 만들기", + quickWecomDetail: "관리자가 스캔을 확인한 후 Maka는 봇 ID와 비밀을 저장하고 영구 연결을 시작합니다.", + quickQqTitle: "모바일QQ로 스캔하여 봇 생성 및 바인딩", + quickQqDetail: "확인 후 QQ는 AppID와 AppSecret을 안전하게 반환합니다. Maka는 이를 로컬에 저장하고 게이트웨이를 시작합니다.", + telegramOfficialFlow: "텔레그램은 공식적으로 @BotFather의 봇 토큰을 요구하며, QR 스캔으로 봇을 생성하고 해당 토큰을 반환하는 API를 제공하지 않습니다.", + quickDetail: "확인 후 Maka는 기본 프로세스에 자격 증명을 저장하고 메시지 연결을 시작합니다.", + feishuRegionAria: "Feishu 계정 지역을 선택하세요", + feishu: "페이슈", + beginQuickBind: "빠른 연결 시작", + scanWith: name => `${name}로 스캔`, + planned: "이 플랫폼은 카탈로그에만 표시됩니다. 활성 채널이나 예약된 작업 전달 대상이 되지 않습니다.", + credentialsSaved: name => `${name} 자격 증명이 저장되었습니다.`, + scanComplete: name => `${name} QR 설정 완료`, + savedAndConnected: "자격 증명이 안전하게 저장되고 연결이 시작되었습니다.", + proxy: "프록시 URL", + chinaRequired: "(중국 본토의 네트워크에 필요)", + authOnly: "(봇 인증만 해당)", + telegramProxyAria: "텔레그램 프록시 URL", + telegramNotice: "네트워크 도구에서 TUN 모드를 활성화하고 앱을 다시 시작하여 Telegram Bot 설정을 완료하세요.", + feishuCredentialId: "Feishu 자격 증명 ID", + feishuSecret: "Feishu 앱 비밀", + feishuDomain: "페이슈 도메인", + feishuOption: "페이슈(feishu.cn)", + discordProxyAria: "디스코드 프록시 URL", + discordNotice: "중국 본토에서 Discord에 액세스하는 경우 위 프록시는 봇 인증만 다룹니다. 메시지 WebSocket에는 시스템 수준 프록시가 필요합니다. TUN 모드를 활성화하고 앱을 다시 시작하세요.", + dingtalkId: "딩톡 앱 키", + dingtalkSecret: "DingTalk 클라이언트 비밀번호", + wecomBotPlaceholder: "WeCom AI 앱 봇 ID", + wecomBotAria: "WeCom 봇 ID", + wecomSecretPlaceholder: "AI 앱 비밀", + wecomSecretAria: "WeCom 비밀", + qqId: "QQ 앱 ID", + allowedUsersLabel: (count, max) => `허용된 사용자 ID(${count} / ${max})`, + allowedUsersPlaceholder: "한 줄에 하나의 사용자 ID; 모든 사람이 허용하려면 비워 두세요.\n예: 123456789", + allowedUsersHelp: "텔레그램 사용자 ID는 64비트 정수입니다. 설정되면 해당 ID의 메시지만 허용됩니다. 다른 모든 것은 자동으로 무시됩니다.", + limitReached: "(한도 도달)", + invalidUsers: values => `다음 항목은 숫자 ID가 아니며 사용자 이름일 수 있으므로 누구와도 일치하지 않습니다. ${values}`, + moreInvalid: count => `그리고 ${count} 더` + }, + onboarding: { + providers: { + dingtalk: { + title: "딩톡 설정", + ariaLabel: "QR 코드로 DingTalk 설정", + qrAlt: "DingTalk 설정 QR 코드", + subtitle: "DingTalk에서 스캔하여 앱을 등록하세요.", + waiting: "DingTalk로 스캔하고 인증을 확인하세요", + scanned: "스캔했습니다. DingTalk에서 확인을 완료하세요." + }, + feishu: { + title: "페이슈 설정", + ariaLabel: "QR 코드로 Feishu 설정", + qrAlt: "Feishu 설정 QR 코드", + subtitle: "Feishu로 스캔하여 봇 생성 및 구성", + waiting: "Feishu로 스캔하고 생성 확인", + scanned: "스캔했습니다. Feishu에서 확인을 완료하세요." + }, + wecom: { + title: "WeCom 설정", + ariaLabel: "QR 코드로 WeCom 설정", + qrAlt: "WeCom 설정 QR 코드", + subtitle: "빠른 설정으로 WeCom 봇 생성 및 연결", + waiting: "WeCom을 열고 스캔하여 봇을 만듭니다.", + scanned: "스캔했습니다. WeCom에서 확인을 완료하세요." + }, + wechat: { + title: "스캔하여 로그인", + ariaLabel: "위챗 QR 로그인", + qrAlt: "WeChat 로그인 QR 코드", + subtitle: "연결하려면 WeChat으로 스캔하세요.", + waiting: "WeChat으로 스캔하고 휴대폰에서 확인하세요", + scanned: "스캔했습니다. WeChat에서 확인을 완료하세요." + }, + qq: { + title: "QQ 설정", + ariaLabel: "QR 코드로 QQ 설정", + qrAlt: "QQ 설정 QR 코드", + subtitle: "모바일QQ로 스캔하여 봇 생성 및 바인딩", + waiting: "모바일QQ로 스캔하고 바인딩을 확인하세요", + scanned: "스캔했습니다. QQ에서 확인을 완료하세요." + } + }, + lark: { + title: "Lark 설정", + ariaLabel: "QR 코드로 Lark 설정", + qrAlt: "Lark 설정 QR 코드", + subtitle: "Lark로 스캔하여 봇 생성 및 구성", + waiting: "Lark로 스캔하고 생성 확인", + scanned: "스캔했습니다. Lark에서 확인을 완료하세요." + }, + connectedRefreshFailed: message => `연결되었지만 상태 새로 고침에 실패했습니다: ${message}`, + close: title => `${title} 닫기`, + generatingAria: "QR 코드 생성 중", + privacy: "자격 증명은 이 장치에 유지되며 렌더러나 Maka 클라우드로 전송되지 않습니다.", + openBrowser: "스캔할 수 없나요? 브라우저에서 열기", + done: "완료", + regenerate: "다시 생성", + refreshQr: "QR 코드 새로 고침", + cancel: "취소", + generating: "보안 QR 코드 생성 중…", + connecting: "승인이 완료되었습니다. 사용자 인증 정보를 저장하고 연결을 시작하는 중…", + connected: name => `${name} 연결됨`, + connectedWarning: "자격 증명이 저장되었지만 연결이 시작되지 않았습니다.", + expired: "QR 코드가 만료되었습니다. 새로운 것을 생성하십시오.", + denied: "승인이 취소되었습니다. 새로운 QR 코드를 생성하세요.", + cancelled: "QR 설정이 취소되었습니다.", + failed: "QR 설정에 실패했습니다. 다시 시도해 보세요.", + preparing: "QR 설정 준비 중…" + }, + wechat: { + token: "위챗 봇 토큰", + tokenPlaceholder: "로컬 wechat-bridge Bearer 토큰", + collapseAdvanced: "고급 설정 숨기기", + expandAdvanced: "고급 설정 (공식 계정 / 로컬 브릿지 URL)", + bridgeAddress: "로컬 브리지 URL", + appId: "공식 계정 앱 ID", + appIdPlaceholder: "WeChat 공식 계정 앱 ID", + appSecret: "공식 계정 앱 비밀", + appSecretPlaceholder: "WeChat 공식 계정 앱 비밀", + advancedNotice: "로컬 브리지의 기본값은 http://127.0.0.1:18400입니다. 공식 계정 앱 ID와 앱 비밀번호는 공식 계정 메시지에만 사용됩니다. 개인 WeChat QR 로그인은 로컬 브리지를 사용합니다.", + readQrFailed: "로컬 wechat-bridge에서 QR 코드를 읽을 수 없습니다. 브릿지가 작동하는지 확인하세요.", + title: "위챗 QR 로그인", + subtitle: "WeChat으로 QR 코드를 스캔하고 휴대폰에서 로컬 wechat-bridge에 로그인했는지 확인하세요.", + close: "WeChat QR 로그인 닫기", + generating: "QR 코드 생성 중…", + loggedIn: "WeChat에 로그인되었습니다. 돌아가서 연결을 테스트하거나 리스너를 다시 시작하세요.", + expired: "QR 코드가 만료되었습니다", + expiredHint: "로그인을 계속하려면 QR 코드를 새로고침하고 다시 스캔하세요.", + refreshing: "상쾌하다…", + refresh: "QR 코드 새로 고침", + qrAlt: "WeChat 로그인 QR 코드", + waiting: "확인 대기 중... 로그인 상태는 3초마다 새로고침됩니다.", + retrying: "재시도 중…", + retry: "다시 해 보다", + bridgeGenerating: "브리지에서 QR 코드를 생성하는 중입니다.", + bridgeGeneratingHint: "준비가 되면 QR 코드가 자동으로 나타납니다. 다시 가져올 수도 있습니다.", + fetching: "가져오는 중…", + fetchAgain: "다시 가져오기" + } + } +} satisfies UiCatalog; export function getBotSettingsCopy(locale: UiLocale): BotSettingsCopy { return BOT_SETTINGS_COPY[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts index 0c015bd453..d4ebff4f6f 100644 --- a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts @@ -18,7 +18,6 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - export type DailyReviewSettingsCopy = { defaultModel: string; saveFailed: string; @@ -38,14 +37,13 @@ export type DailyReviewSettingsCopy = { model: string; modelHelp: string; }; - -const SETTINGS_DAILY_REVIEW_COPY = { +const SETTINGS_DAILY_REVIEW_COPY_BASE = { zh: { defaultModel: '跟随任务默认', saveFailed: '保存每日回顾设置失败', aria: '每日回顾', unavailable: '当前版本无法读取每日回顾设置。', - loadFailed: (error) => `读取每日回顾设置失败:${error}`, + loadFailed: error => `读取每日回顾设置失败:${error}`, scheduleTitle: '定时分析', scheduleDescription: '按本地时间自动分析前一个完整自然日的活动。', enabled: '启用定时分析', @@ -57,14 +55,14 @@ const SETTINGS_DAILY_REVIEW_COPY = { analysisTitle: '分析', analysisDescription: '选择用于生成固定结构报告的模型。', model: '分析模型', - modelHelp: '未指定时跟随当前任务的默认模型。', + modelHelp: '未指定时跟随当前任务的默认模型。' }, en: { defaultModel: 'Follow task default', saveFailed: 'Failed to save Daily Review settings', aria: 'Daily Review', unavailable: 'Daily Review settings are unavailable in this build.', - loadFailed: (error) => `Failed to load Daily Review settings: ${error}`, + loadFailed: error => `Failed to load Daily Review settings: ${error}`, scheduleTitle: 'Schedule', scheduleDescription: 'Analyze the previous complete local day automatically.', enabled: 'Enable scheduled analysis', @@ -76,10 +74,31 @@ const SETTINGS_DAILY_REVIEW_COPY = { analysisTitle: 'Analysis', analysisDescription: 'Choose the model used to generate the fixed report structure.', model: 'Analysis model', - modelHelp: 'Follows the current task default when unspecified.', - }, + modelHelp: 'Follows the current task default when unspecified.' + } +} satisfies Omit, 'ko'>; +const SETTINGS_DAILY_REVIEW_COPY = { + ...SETTINGS_DAILY_REVIEW_COPY_BASE, + ko: { + defaultModel: "작업 기본값 따르기", + saveFailed: "일일 검토 설정을 저장하지 못했습니다.", + aria: "일일 검토", + unavailable: "이 빌드에서는 일일 검토 설정을 사용할 수 없습니다.", + loadFailed: error => `일일 검토 설정을 로드하지 못했습니다: ${error}`, + scheduleTitle: "일정", + scheduleDescription: "이전 전체 현지 날짜를 자동으로 분석합니다.", + enabled: "예약된 분석 활성화", + enabledHelp: "매일 어제의 활동에 대한 분석을 생성합니다.", + executeTime: "런타임", + executeTimeHelp: "현지 시간을 24시간 형식으로 사용합니다.", + executeTimePlaceholder: 'HH:mm', + executeTimeInvalid: "24시간제 시간을 입력합니다(예: 08:00).", + analysisTitle: "분석", + analysisDescription: "고정된 보고서 구조를 생성하는 데 사용되는 모델을 선택합니다.", + model: "분석 모델", + modelHelp: "지정되지 않은 경우 현재 작업 기본값을 따릅니다." + } } satisfies UiCatalog; - export function getDailyReviewSettingsCopy(locale: UiLocale): DailyReviewSettingsCopy { return SETTINGS_DAILY_REVIEW_COPY[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-data-copy.ts b/apps/desktop/src/renderer/locales/settings-data-copy.ts index 7f38a86812..9622794a2f 100644 --- a/apps/desktop/src/renderer/locales/settings-data-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-data-copy.ts @@ -19,83 +19,285 @@ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; import type { ConfigCategory } from '@maka/storage/config-transfer'; - export type DataSettingsCopy = { - categories: Record; + categories: Record; importSummary: { connections(created: number, overwritten: number, skipped: number): string; - settings: string; credentials(applied: number, skipped: number): string; memory: string; empty: string; + settings: string; + credentials(applied: number, skipped: number): string; + memory: string; + empty: string; }; - loadFailed: string; openFailed(label: string): string; pathCopied: string; copyFailed: string; copyFailedDetail: string; - historyCleared: string; historyClearedDetail: string; selectCategory: string; exported: string; exportedDetail(items: readonly string[]): string; - exportFailed: string; noCategories: string; tryAgain: string; imported: string; importFailed: string; invalidFile: string; + loadFailed: string; + openFailed(label: string): string; + pathCopied: string; + copyFailed: string; + copyFailedDetail: string; + historyCleared: string; + historyClearedDetail: string; + selectCategory: string; + exported: string; + exportedDetail(items: readonly string[]): string; + exportFailed: string; + noCategories: string; + tryAgain: string; + imported: string; + importFailed: string; + invalidFile: string; rows: { - workspace: string; workspaceDetail: string; loadValueFailed: string; loading: string; - history: string; historyDetail: string; + workspace: string; + workspaceDetail: string; + loadValueFailed: string; + loading: string; + history: string; + historyDetail: string; }; - actionsAria: string; opening: string; openWorkspace: string; copying: string; copyPath: string; clearing: string; clearHistory: string; - backupTitle: string; backupNotice: string; pathLoadFailed(error: string): string; configAria: string; configTitle: string; configHelp: string; categoryAria: string; - sensitiveWarning: string; conflictAria: string; skip: string; overwrite: string; - exportConfig: string; importConfig: string; + actionsAria: string; + opening: string; + openWorkspace: string; + copying: string; + copyPath: string; + clearing: string; + clearHistory: string; + backupTitle: string; + backupNotice: string; + pathLoadFailed(error: string): string; + configAria: string; + configTitle: string; + configHelp: string; + categoryAria: string; + sensitiveWarning: string; + conflictAria: string; + skip: string; + overwrite: string; + exportConfig: string; + importConfig: string; }; - -const SETTINGS_DATA_COPY = { +const SETTINGS_DATA_COPY_BASE = { zh: { categories: { - connections: { label: '模型连接', detail: '供应商连接与默认模型(不含密钥)' }, - settings: { label: '应用设置', detail: '常规、搜索、机器人、代理等设置' }, - memory: { label: '本地记忆', detail: '本机 MEMORY.md 的内容' }, - credentials: { label: '凭据(API 密钥、令牌)', detail: '模型密钥与订阅令牌等敏感信息', sensitive: true }, + connections: { + label: '模型连接', + detail: '供应商连接与默认模型(不含密钥)' + }, + settings: { + label: '应用设置', + detail: '常规、搜索、机器人、代理等设置' + }, + memory: { + label: '本地记忆', + detail: '本机 MEMORY.md 的内容' + }, + credentials: { + label: '凭据(API 密钥、令牌)', + detail: '模型密钥与订阅令牌等敏感信息', + sensitive: true + } }, importSummary: { connections: (created, overwritten, skipped) => `连接 新增${created}·覆盖${overwritten}·跳过${skipped}`, - settings: '设置已应用', credentials: (applied, skipped) => skipped > 0 ? `凭据 ${applied}(跳过 ${skipped})` : `凭据 ${applied}`, - memory: '记忆已应用', empty: '文件不含可导入的内容', + settings: '设置已应用', + credentials: (applied, skipped) => skipped > 0 ? `凭据 ${applied}(跳过 ${skipped})` : `凭据 ${applied}`, + memory: '记忆已应用', + empty: '文件不含可导入的内容' }, - loadFailed: '载入数据目录失败', openFailed: (label) => `无法打开${label}`, pathCopied: '已复制工作区路径', copyFailed: '复制失败', copyFailedDetail: '剪贴板不可用或被系统拒绝。', - historyCleared: '已清空输入历史', historyClearedDetail: '已发送的提示词记录已从本机移除。', selectCategory: '请至少选择一个类别', - exported: '已导出配置', exportedDetail: (items) => `包含:${items.join('、')}`, exportFailed: '导出失败', noCategories: '未选择任何类别', tryAgain: '请稍后重试', - imported: '已导入配置', importFailed: '导入失败', invalidFile: '文件无效或版本不受支持。', + loadFailed: '载入数据目录失败', + openFailed: label => `无法打开${label}`, + pathCopied: '已复制工作区路径', + copyFailed: '复制失败', + copyFailedDetail: '剪贴板不可用或被系统拒绝。', + historyCleared: '已清空输入历史', + historyClearedDetail: '已发送的提示词记录已从本机移除。', + selectCategory: '请至少选择一个类别', + exported: '已导出配置', + exportedDetail: items => `包含:${items.join('、')}`, + exportFailed: '导出失败', + noCategories: '未选择任何类别', + tryAgain: '请稍后重试', + imported: '已导入配置', + importFailed: '导入失败', + invalidFile: '文件无效或版本不受支持。', rows: { - workspace: '工作区路径', workspaceDetail: '任务、设置、凭据和技能文件都存在这个目录下。', loadValueFailed: '载入失败', loading: '正在加载…', - history: '输入历史', historyDetail: '上箭头 / 下箭头调出的已发送提示词记录,保存在本机、重启后仍在。清空后无法恢复。', + workspace: '工作区路径', + workspaceDetail: '任务、设置、凭据和技能文件都存在这个目录下。', + loadValueFailed: '载入失败', + loading: '正在加载…', + history: '输入历史', + historyDetail: '上箭头 / 下箭头调出的已发送提示词记录,保存在本机、重启后仍在。清空后无法恢复。' }, - actionsAria: '工作区数据操作', opening: '打开中…', openWorkspace: '打开工作区文件夹', copying: '复制中…', copyPath: '复制路径', clearing: '清空中…', clearHistory: '清空输入历史', - backupTitle: '备份与恢复', backupNotice: '本机数据保存在工作区。需要备份时先退出 Maka,再复制整个目录;恢复时替换同一路径后重启。模型连接凭据随工作区恢复后需要重新测试;订阅账号令牌通常需要重新登录。', - pathLoadFailed: (error) => `无法载入工作区路径:${error}`, configAria: '配置导入导出', configTitle: '配置导入导出', - configHelp: '勾选要导出的内容,生成一个 JSON 备份文件;换机或重装时可再导入。默认不含密钥。', categoryAria: '选择导出内容', + actionsAria: '工作区数据操作', + opening: '打开中…', + openWorkspace: '打开工作区文件夹', + copying: '复制中…', + copyPath: '复制路径', + clearing: '清空中…', + clearHistory: '清空输入历史', + backupTitle: '备份与恢复', + backupNotice: '本机数据保存在工作区。需要备份时先退出 Maka,再复制整个目录;恢复时替换同一路径后重启。模型连接凭据随工作区恢复后需要重新测试;订阅账号令牌通常需要重新登录。', + pathLoadFailed: error => `无法载入工作区路径:${error}`, + configAria: '配置导入导出', + configTitle: '配置导入导出', + configHelp: '勾选要导出的内容,生成一个 JSON 备份文件;换机或重装时可再导入。默认不含密钥。', + categoryAria: '选择导出内容', sensitiveWarning: '⚠️ 密钥将以明文写入导出文件。任何拿到该文件的人都能使用这些密钥,请妥善保管、不要分享。', - conflictAria: '导入时同名连接的处理方式', skip: '跳过', overwrite: '覆盖', exportConfig: '导出配置…', importConfig: '导入配置…', + conflictAria: '导入时同名连接的处理方式', + skip: '跳过', + overwrite: '覆盖', + exportConfig: '导出配置…', + importConfig: '导入配置…' }, en: { categories: { - connections: { label: 'Model connections', detail: 'Provider connections and default models (without secrets)' }, - settings: { label: 'App settings', detail: 'General, search, bot, proxy, and other settings' }, - memory: { label: 'Local memory', detail: 'Contents of the local MEMORY.md file' }, - credentials: { label: 'Credentials (API keys and tokens)', detail: 'Sensitive model keys and subscription tokens', sensitive: true }, + connections: { + label: 'Model connections', + detail: 'Provider connections and default models (without secrets)' + }, + settings: { + label: 'App settings', + detail: 'General, search, bot, proxy, and other settings' + }, + memory: { + label: 'Local memory', + detail: 'Contents of the local MEMORY.md file' + }, + credentials: { + label: 'Credentials (API keys and tokens)', + detail: 'Sensitive model keys and subscription tokens', + sensitive: true + } }, importSummary: { connections: (created, overwritten, skipped) => `Connections: ${created} created · ${overwritten} overwritten · ${skipped} skipped`, - settings: 'Settings applied', credentials: (applied, skipped) => skipped > 0 ? `Credentials: ${applied} applied (${skipped} skipped)` : `Credentials: ${applied} applied`, - memory: 'Memory applied', empty: 'The file contains no importable data', + settings: 'Settings applied', + credentials: (applied, skipped) => skipped > 0 ? `Credentials: ${applied} applied (${skipped} skipped)` : `Credentials: ${applied} applied`, + memory: 'Memory applied', + empty: 'The file contains no importable data' }, - loadFailed: 'Failed to load data directory', openFailed: (label) => `Could not open ${label}`, pathCopied: 'Workspace path copied', copyFailed: 'Copy failed', copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', - historyCleared: 'Input history cleared', historyClearedDetail: 'Sent prompt history was removed from this device.', selectCategory: 'Select at least one category', - exported: 'Configuration exported', exportedDetail: (items) => `Included: ${items.join(', ')}`, exportFailed: 'Export failed', noCategories: 'No categories selected', tryAgain: 'Try again later', - imported: 'Configuration imported', importFailed: 'Import failed', invalidFile: 'The file is invalid or its version is unsupported.', + loadFailed: 'Failed to load data directory', + openFailed: label => `Could not open ${label}`, + pathCopied: 'Workspace path copied', + copyFailed: 'Copy failed', + copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', + historyCleared: 'Input history cleared', + historyClearedDetail: 'Sent prompt history was removed from this device.', + selectCategory: 'Select at least one category', + exported: 'Configuration exported', + exportedDetail: items => `Included: ${items.join(', ')}`, + exportFailed: 'Export failed', + noCategories: 'No categories selected', + tryAgain: 'Try again later', + imported: 'Configuration imported', + importFailed: 'Import failed', + invalidFile: 'The file is invalid or its version is unsupported.', rows: { - workspace: 'Workspace path', workspaceDetail: 'Tasks, settings, credentials, and skill files are stored in this directory.', loadValueFailed: 'Failed to load', loading: 'Loading…', - history: 'Input history', historyDetail: 'Previously sent prompts recalled with the Up and Down arrows are kept on this machine and persist across restarts. Clearing them cannot be undone.', + workspace: 'Workspace path', + workspaceDetail: 'Tasks, settings, credentials, and skill files are stored in this directory.', + loadValueFailed: 'Failed to load', + loading: 'Loading…', + history: 'Input history', + historyDetail: 'Previously sent prompts recalled with the Up and Down arrows are kept on this machine and persist across restarts. Clearing them cannot be undone.' }, - actionsAria: 'Workspace data actions', opening: 'Opening…', openWorkspace: 'Open workspace folder', copying: 'Copying…', copyPath: 'Copy path', clearing: 'Clearing…', clearHistory: 'Clear input history', - backupTitle: 'Backup and restore', backupNotice: 'Local data is stored in the workspace. To back it up, quit Maka and copy the entire directory. To restore it, replace the same path and restart. Model credentials should be tested again after a restore, and subscription accounts usually need to sign in again.', - pathLoadFailed: (error) => `Could not load workspace path: ${error}`, configAria: 'Configuration import and export', configTitle: 'Configuration import and export', - configHelp: 'Select the content to export into a JSON backup. You can import it after moving devices or reinstalling. Secrets are excluded by default.', categoryAria: 'Select export content', + actionsAria: 'Workspace data actions', + opening: 'Opening…', + openWorkspace: 'Open workspace folder', + copying: 'Copying…', + copyPath: 'Copy path', + clearing: 'Clearing…', + clearHistory: 'Clear input history', + backupTitle: 'Backup and restore', + backupNotice: 'Local data is stored in the workspace. To back it up, quit Maka and copy the entire directory. To restore it, replace the same path and restart. Model credentials should be tested again after a restore, and subscription accounts usually need to sign in again.', + pathLoadFailed: error => `Could not load workspace path: ${error}`, + configAria: 'Configuration import and export', + configTitle: 'Configuration import and export', + configHelp: 'Select the content to export into a JSON backup. You can import it after moving devices or reinstalling. Secrets are excluded by default.', + categoryAria: 'Select export content', sensitiveWarning: '⚠️ Secrets will be written to the export file as plain text. Anyone with this file can use them. Store it securely and do not share it.', - conflictAria: 'How to handle connections with the same name during import', skip: 'Skip', overwrite: 'Overwrite', exportConfig: 'Export configuration…', importConfig: 'Import configuration…', - }, + conflictAria: 'How to handle connections with the same name during import', + skip: 'Skip', + overwrite: 'Overwrite', + exportConfig: 'Export configuration…', + importConfig: 'Import configuration…' + } +} satisfies Omit, 'ko'>; +const SETTINGS_DATA_COPY = { + ...SETTINGS_DATA_COPY_BASE, + ko: { + categories: { + connections: { + label: "모델 연결", + detail: "공급자 연결 및 기본 모델(비밀 없음)" + }, + settings: { + label: "앱 설정", + detail: "일반, 검색, 봇, 프록시 및 기타 설정" + }, + memory: { + label: "로컬 메모리", + detail: "로컬 MEMORY.md 파일의 내용" + }, + credentials: { + label: "자격 증명(API 키 및 토큰)", + detail: "민감한 모델 키 및 구독 토큰", + sensitive: true + } + }, + importSummary: { + connections: (created, overwritten, skipped) => `연결: ${created} 생성됨 · ${overwritten} 덮어쓰기 · ${skipped} 건너뛰기`, + settings: "설정이 적용되었습니다.", + credentials: (applied, skipped) => skipped > 0 ? `자격 증명: ${applied} 적용됨(${skipped} 건너뛰기)` : `자격 증명: ${applied} 적용됨`, + memory: "메모리 적용됨", + empty: "파일에 가져올 수 있는 데이터가 없습니다." + }, + loadFailed: "데이터 디렉터리를 로드하지 못했습니다.", + openFailed: label => `${label}을(를) 열 수 없습니다.`, + pathCopied: "작업공간 경로가 복사되었습니다.", + copyFailed: "복사 실패", + copyFailedDetail: "클립보드를 사용할 수 없거나 시스템에서 액세스를 거부했습니다.", + historyCleared: "입력 기록이 삭제되었습니다.", + historyClearedDetail: "전송된 메시지 기록이 이 기기에서 삭제되었습니다.", + selectCategory: "카테고리를 하나 이상 선택하세요.", + exported: "내보낸 구성", + exportedDetail: items => `포함됨: ${items.join(', ')}`, + exportFailed: "내보내기 실패", + noCategories: "선택한 카테고리가 없습니다.", + tryAgain: "나중에 다시 시도해 보세요", + imported: "가져온 구성", + importFailed: "가져오기 실패", + invalidFile: "파일이 잘못되었거나 해당 버전이 지원되지 않습니다.", + rows: { + workspace: "작업공간 경로", + workspaceDetail: "작업, 설정, 자격 증명 및 기술 파일이 이 디렉터리에 저장됩니다.", + loadValueFailed: "로드하지 못했습니다.", + loading: "로드 중…", + history: "입력 이력", + historyDetail: "위쪽 및 아래쪽 화살표를 사용하여 호출한 이전에 전송된 프롬프트는 이 시스템에 유지되며 다시 시작해도 지속됩니다. 삭제한 후에는 취소할 수 없습니다." + }, + actionsAria: "작업공간 데이터 작업", + opening: "열기…", + openWorkspace: "작업공간 폴더 열기", + copying: "사자…", + copyPath: "경로 복사", + clearing: "청산…", + clearHistory: "입력 기록 지우기", + backupTitle: "백업 및 복원", + backupNotice: "로컬 데이터는 작업 공간에 저장됩니다. 백업하려면 Maka를 종료하고 전체 디렉터리를 복사하세요. 복원하려면 동일한 경로를 교체하고 다시 시작하세요. 모델 자격 증명은 복원 후 다시 테스트해야 하며 일반적으로 구독 계정은 다시 로그인해야 합니다.", + pathLoadFailed: error => `작업공간 경로를 로드할 수 없습니다: ${error}`, + configAria: "구성 가져오기 및 내보내기", + configTitle: "구성 가져오기 및 내보내기", + configHelp: "JSON 백업으로 내보낼 콘텐츠를 선택하세요. 기기를 이동하거나 재설치한 후 가져올 수 있습니다. 비밀은 기본적으로 제외됩니다.", + categoryAria: "내보내기 내용 선택", + sensitiveWarning: "⚠️ 비밀은 내보내기 파일에 일반 텍스트로 기록됩니다. 이 파일이 있는 사람은 누구나 사용할 수 있습니다. 안전하게 보관하고 공유하지 마세요.", + conflictAria: "가져오는 동안 동일한 이름의 연결을 처리하는 방법", + skip: "건너뛰다", + overwrite: "덮어쓰기", + exportConfig: "구성 내보내기…", + importConfig: "구성 가져오기…" + } } satisfies UiCatalog; - export function getDataSettingsCopy(locale: UiLocale): DataSettingsCopy { return SETTINGS_DATA_COPY[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-health-copy.ts b/apps/desktop/src/renderer/locales/settings-health-copy.ts index 6bb4aebbb9..f9bbe50fda 100644 --- a/apps/desktop/src/renderer/locales/settings-health-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-health-copy.ts @@ -19,9 +19,7 @@ import type { StatusSemantic } from '@maka/ui'; import type { HealthSignal, HealthSignalLayer, HealthSignalSource, HealthSignalStatus } from '@maka/core/health'; - import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - /** * Health signals carry their own severity ladder — error > warning > info > ok * — and the colours have to stay monotonic with it. That is why `info` maps to @@ -32,7 +30,6 @@ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; * for action. */ type HealthTone = StatusSemantic; - export type HealthCenterCopy = { loading: string; readFailed: string; @@ -52,8 +49,14 @@ export type HealthCenterCopy = { layerAria(label: string): string; layerListAria(label: string): string; footnote: string; - layers: Record; - statuses: Record; + layers: Record; + statuses: Record; scopes: Record; sources: Record; source: string; @@ -63,79 +66,316 @@ export type HealthCenterCopy = { signalMessage(signal: HealthSignal): string; signalDetail(signal: HealthSignal): string | undefined; }; - const layersZh: HealthCenterCopy['layers'] = { - configuration: { label: '配置', description: '是否填齐了设置页里的必填项。' }, - validation: { label: '验证', description: '凭据 / 端点的连通性测试结果,仅代表验证通过,不等于发送通路可用。' }, - permission: { label: '系统权限', description: '所需 OS / TCC 权限是否已授权。' }, - feature: { label: '功能开关', description: '功能是否被显式启用、当前是否可使用。' }, - action_approval: { label: '操作审批', description: '每次工具调用 / 高危操作的审批策略状态。' }, - memory_acceptance: { label: '记忆写入', description: '是否接受了记忆写入约定、是否启用了记忆写入。' }, - runtime_probe: { label: '运行态探测', description: '最近一次真实运行(发送 / 流式 / 接收事件)的探测结果。' }, - storage: { label: '存储', description: '工作区文件、JSONL、SQLite 等本地存储健康度。' }, + configuration: { + label: '配置', + description: '是否填齐了设置页里的必填项。' + }, + validation: { + label: '验证', + description: '凭据 / 端点的连通性测试结果,仅代表验证通过,不等于发送通路可用。' + }, + permission: { + label: '系统权限', + description: '所需 OS / TCC 权限是否已授权。' + }, + feature: { + label: '功能开关', + description: '功能是否被显式启用、当前是否可使用。' + }, + action_approval: { + label: '操作审批', + description: '每次工具调用 / 高危操作的审批策略状态。' + }, + memory_acceptance: { + label: '记忆写入', + description: '是否接受了记忆写入约定、是否启用了记忆写入。' + }, + runtime_probe: { + label: '运行态探测', + description: '最近一次真实运行(发送 / 流式 / 接收事件)的探测结果。' + }, + storage: { + label: '存储', + description: '工作区文件、JSONL、SQLite 等本地存储健康度。' + } }; - const layersEn: HealthCenterCopy['layers'] = { - configuration: { label: 'Configuration', description: 'Whether required settings are complete.' }, - validation: { label: 'Validation', description: 'Credential and endpoint connectivity results. A passing validation does not prove the send path works.' }, - permission: { label: 'System permissions', description: 'Whether required OS and TCC permissions are granted.' }, - feature: { label: 'Feature state', description: 'Whether the feature is explicitly enabled and currently available.' }, - action_approval: { label: 'Action approval', description: 'Approval policy for tool calls and high-risk actions.' }, - memory_acceptance: { label: 'Memory writes', description: 'Whether the memory-write agreement was accepted and writes are enabled.' }, - runtime_probe: { label: 'Runtime probe', description: 'The latest real send, stream, or event-receipt observation.' }, - storage: { label: 'Storage', description: 'Health of workspace files, JSONL, SQLite, and other local storage.' }, + configuration: { + label: 'Configuration', + description: 'Whether required settings are complete.' + }, + validation: { + label: 'Validation', + description: 'Credential and endpoint connectivity results. A passing validation does not prove the send path works.' + }, + permission: { + label: 'System permissions', + description: 'Whether required OS and TCC permissions are granted.' + }, + feature: { + label: 'Feature state', + description: 'Whether the feature is explicitly enabled and currently available.' + }, + action_approval: { + label: 'Action approval', + description: 'Approval policy for tool calls and high-risk actions.' + }, + memory_acceptance: { + label: 'Memory writes', + description: 'Whether the memory-write agreement was accepted and writes are enabled.' + }, + runtime_probe: { + label: 'Runtime probe', + description: 'The latest real send, stream, or event-receipt observation.' + }, + storage: { + label: 'Storage', + description: 'Health of workspace files, JSONL, SQLite, and other local storage.' + } }; - -const SETTINGS_HEALTH_COPY = { +const SETTINGS_HEALTH_COPY_BASE = { zh: { - loading: '正在加载健康快照', readFailed: '无法读取健康快照', noData: '健康服务未返回数据。', readAgain: '重新读取', - title: '健康中心', subtitle: '各项能力当前的运行状况检查。', - badge: '只读快照', lastRead: '最近一次读取:', refresh: '刷新', summaryAria: '按状态筛选健康信号', summaryFilterAria: (label, count, selected) => selected ? `${label} ${count} 项,当前筛选;再次按下显示全部` : `仅显示${label}健康信号,共 ${count} 项`, + loading: '正在加载健康快照', + readFailed: '无法读取健康快照', + noData: '健康服务未返回数据。', + readAgain: '重新读取', + title: '健康中心', + subtitle: '各项能力当前的运行状况检查。', + badge: '只读快照', + lastRead: '最近一次读取:', + refresh: '刷新', + summaryAria: '按状态筛选健康信号', + summaryFilterAria: (label, count, selected) => selected ? `${label} ${count} 项,当前筛选;再次按下显示全部` : `仅显示${label}健康信号,共 ${count} 项`, blockers: { send: (count, totalCount) => `全部健康信号中,${count}/${totalCount} 条会阻塞发送`, - capability: (count, totalCount) => `全部健康信号中,${count}/${totalCount} 条会阻塞能力`, + capability: (count, totalCount) => `全部健康信号中,${count}/${totalCount} 条会阻塞能力` }, - layerAria: (label) => `${label}健康信号`, layerListAria: (label) => `${label}健康信号列表`, + layerAria: label => `${label}健康信号`, + layerListAria: label => `${label}健康信号列表`, footnote: '本页不直接执行测试、修复或权限变更;它只汇总当前已记录的健康信号。需要处理问题时,请进入对应设置页或重新触发相关功能。', layers: layersZh, - statuses: { ok: { label: '正常', tone: 'neutral' }, info: { label: '提示', tone: 'neutral' }, warning: { label: '警告', tone: 'attention' }, error: { label: '错误', tone: 'error' }, unknown: { label: '未知', tone: 'neutral' } }, - scopes: { app: '应用', llm_connection: 'LLM 连接', bot: '机器人', capability: '能力', storage: '存储' }, - sources: { connection_test: '连接测试', capability_snapshot: '能力快照', permission_snapshot: '权限快照', runtime_probe: '运行态探测', settings: '设置', storage: '本地存储' }, - source: '来源:', blocksSend: '阻塞发送', blocksCapability: '阻塞能力', - signalLabel: (signal) => signal.label, - signalMessage: (signal) => signal.message, - signalDetail: (signal) => signal.detail, + statuses: { + ok: { + label: '正常', + tone: 'neutral' + }, + info: { + label: '提示', + tone: 'neutral' + }, + warning: { + label: '警告', + tone: 'attention' + }, + error: { + label: '错误', + tone: 'error' + }, + unknown: { + label: '未知', + tone: 'neutral' + } + }, + scopes: { + app: '应用', + llm_connection: 'LLM 连接', + bot: '机器人', + capability: '能力', + storage: '存储' + }, + sources: { + connection_test: '连接测试', + capability_snapshot: '能力快照', + permission_snapshot: '权限快照', + runtime_probe: '运行态探测', + settings: '设置', + storage: '本地存储' + }, + source: '来源:', + blocksSend: '阻塞发送', + blocksCapability: '阻塞能力', + signalLabel: signal => signal.label, + signalMessage: signal => signal.message, + signalDetail: signal => signal.detail }, en: { - loading: 'Loading health snapshot', readFailed: 'Could not read health snapshot', noData: 'The health service returned no data.', readAgain: 'Read again', - title: 'Health center', subtitle: 'How each capability is currently doing.', - badge: 'Read-only snapshot', lastRead: 'Last read: ', refresh: 'Refresh', summaryAria: 'Filter health signals by status', summaryFilterAria: (label, count, selected) => selected ? `${label}, ${count}; filter selected. Press again to show all signals` : `Show only ${label.toLowerCase()} health signals, ${count}`, + loading: 'Loading health snapshot', + readFailed: 'Could not read health snapshot', + noData: 'The health service returned no data.', + readAgain: 'Read again', + title: 'Health center', + subtitle: 'How each capability is currently doing.', + badge: 'Read-only snapshot', + lastRead: 'Last read: ', + refresh: 'Refresh', + summaryAria: 'Filter health signals by status', + summaryFilterAria: (label, count, selected) => selected ? `${label}, ${count}; filter selected. Press again to show all signals` : `Show only ${label.toLowerCase()} health signals, ${count}`, blockers: { send: (count, totalCount) => `Across all health signals, ${count} of ${totalCount} ${count === 1 ? 'blocks' : 'block'} sending`, - capability: (count, totalCount) => `Across all health signals, ${count} of ${totalCount} ${count === 1 ? 'blocks' : 'block'} capabilities`, + capability: (count, totalCount) => `Across all health signals, ${count} of ${totalCount} ${count === 1 ? 'blocks' : 'block'} capabilities` }, - layerAria: (label) => `${label} health signals`, layerListAria: (label) => `${label} health signal list`, + layerAria: label => `${label} health signals`, + layerListAria: label => `${label} health signal list`, footnote: 'This page does not run tests, repairs, or permission changes. It only summarizes recorded health signals. Open the relevant settings page or retry the related feature to address an issue.', layers: layersEn, - statuses: { ok: { label: 'Healthy', tone: 'neutral' }, info: { label: 'Info', tone: 'neutral' }, warning: { label: 'Warning', tone: 'attention' }, error: { label: 'Error', tone: 'error' }, unknown: { label: 'Unknown', tone: 'neutral' } }, - scopes: { app: 'App', llm_connection: 'LLM connection', bot: 'Bot', capability: 'Capability', storage: 'Storage' }, - sources: { connection_test: 'Connection test', capability_snapshot: 'Capability snapshot', permission_snapshot: 'Permission snapshot', runtime_probe: 'Runtime probe', settings: 'Settings', storage: 'Local storage' }, - source: 'Source: ', blocksSend: 'Blocks sending', blocksCapability: 'Blocks capability', + statuses: { + ok: { + label: 'Healthy', + tone: 'neutral' + }, + info: { + label: 'Info', + tone: 'neutral' + }, + warning: { + label: 'Warning', + tone: 'attention' + }, + error: { + label: 'Error', + tone: 'error' + }, + unknown: { + label: 'Unknown', + tone: 'neutral' + } + }, + scopes: { + app: 'App', + llm_connection: 'LLM connection', + bot: 'Bot', + capability: 'Capability', + storage: 'Storage' + }, + sources: { + connection_test: 'Connection test', + capability_snapshot: 'Capability snapshot', + permission_snapshot: 'Permission snapshot', + runtime_probe: 'Runtime probe', + settings: 'Settings', + storage: 'Local storage' + }, + source: 'Source: ', + blocksSend: 'Blocks sending', + blocksCapability: 'Blocks capability', signalLabel: englishSignalLabel, signalMessage: englishSignalMessage, - signalDetail: englishSignalDetail, - }, + signalDetail: englishSignalDetail + } +} satisfies Omit, 'ko'>; +const SETTINGS_HEALTH_COPY = { + ...SETTINGS_HEALTH_COPY_BASE, + ko: { + loading: "상태 스냅샷 로드 중", + readFailed: "상태 스냅샷을 읽을 수 없습니다.", + noData: "의료 서비스에서 데이터를 반환하지 않았습니다.", + readAgain: "다시 읽어보세요", + title: "건강 센터", + subtitle: "각 기능이 현재 어떻게 수행되고 있는지입니다.", + badge: "읽기 전용 스냅샷", + lastRead: "마지막으로 읽은 날짜:", + refresh: "새로 고치다", + summaryAria: "상태별로 상태 신호 필터링", + summaryFilterAria: (label, count, selected) => selected ? `${label}, ${count}; 필터가 선택되었습니다. 모든 신호를 표시하려면 다시 누르세요.` : `${label.toLowerCase()} 건강 신호, ${count}만 표시`, + blockers: { + send: (count, totalCount) => `전체 건강 신호 ${totalCount}개 중 ${count}개가 전송을 차단합니다.`, + capability: (count, totalCount) => `전체 건강 신호 ${totalCount}개 중 ${count}개가 기능을 차단합니다.` + }, + layerAria: label => `${label} 건강 신호`, + layerListAria: label => `${label} 건강 신호 목록`, + footnote: "이 페이지에서는 테스트, 복구 또는 권한 변경을 실행하지 않습니다. 기록된 건강 신호만 요약합니다. 관련 설정 페이지를 열거나 관련 기능을 다시 시도하여 문제를 해결하세요.", + layers: { + configuration: { + label: "구성", + description: "필수 설정이 완료되었는지 여부." + }, + validation: { + label: "확인", + description: "자격 증명 및 엔드포인트 연결 결과. 유효성 검사를 통과했다고 해서 전송 경로가 작동한다는 것을 증명하는 것은 아닙니다." + }, + permission: { + label: "시스템 권한", + description: "필수 OS 및 TCC 권한 부여 여부." + }, + feature: { + label: "기능 상태", + description: "기능이 명시적으로 활성화되어 있고 현재 사용 가능한지 여부입니다." + }, + action_approval: { + label: "조치 승인", + description: "도구 호출 및 고위험 작업에 대한 승인 정책입니다." + }, + memory_acceptance: { + label: "메모리 쓰기", + description: "메모리 쓰기 계약이 승인되었고 쓰기가 활성화되었는지 여부입니다." + }, + runtime_probe: { + label: "런타임 프로브", + description: "최신 실제 전송, 스트림 또는 이벤트 수신 관찰입니다." + }, + storage: { + label: "저장", + description: "작업공간 파일, JSONL, SQLite 및 기타 로컬 스토리지의 상태입니다." + } + }, + statuses: { + ok: { + label: "건강한", + tone: 'neutral' + }, + info: { + label: "정보", + tone: 'neutral' + }, + warning: { + label: "경고", + tone: 'attention' + }, + error: { + label: "오류", + tone: 'error' + }, + unknown: { + label: "알려지지 않은", + tone: 'neutral' + } + }, + scopes: { + app: "앱", + llm_connection: "LLM 연결", + bot: "봇", + capability: "능력", + storage: "저장" + }, + sources: { + connection_test: "연결 테스트", + capability_snapshot: "기능 스냅샷", + permission_snapshot: "권한 스냅샷", + runtime_probe: "런타임 프로브", + settings: "설정", + storage: "로컬 저장소" + }, + source: "원천:", + blocksSend: "전송 차단", + blocksCapability: "블록 기능", + signalLabel: koreanSignalLabel, + signalMessage: koreanSignalMessage, + signalDetail: koreanSignalDetail + } } satisfies UiCatalog; - export function getHealthCenterCopy(locale: UiLocale): HealthCenterCopy { return SETTINGS_HEALTH_COPY[locale]; } - function englishSignalLabel(signal: HealthSignal): string { if (signal.id.endsWith(':runtime')) return `${signal.label.replace(/\s*运行态$/, '')} runtime`; return signal.label; } +function koreanSignalLabel(signal: HealthSignal): string { + if (signal.id.endsWith(':runtime')) return `${signal.label.replace(/\s*运行态$/u, '')} 런타임`; + return signal.label; +} function englishSignalMessage(signal: HealthSignal): string { if (signal.scope === 'llm_connection') { if (signal.layer === 'configuration') { @@ -153,16 +393,39 @@ function englishSignalMessage(signal: HealthSignal): string { return signal.status === 'info' ? 'Connection is disabled.' : 'Select a default model.'; } if (signal.layer === 'runtime_probe') { - return { ok: 'The latest send completed.', info: 'The latest send was stopped by the user.', warning: 'The latest send failed.', error: 'The latest send failed.', unknown: 'Waiting for a send-path runtime probe.' }[signal.status]; + return { + ok: 'The latest send completed.', + info: 'The latest send was stopped by the user.', + warning: 'The latest send failed.', + error: 'The latest send failed.', + unknown: 'Waiting for a send-path runtime probe.' + }[signal.status]; } - return { ok: 'Credentials and endpoint validation passed.', info: 'Connection validation needs attention.', warning: 'The latest connection validation failed.', error: 'The connection needs authentication repair.', unknown: 'Waiting to validate the connection.' }[signal.status]; + return { + ok: 'Credentials and endpoint validation passed.', + info: 'Connection validation needs attention.', + warning: 'The latest connection validation failed.', + error: 'The connection needs authentication repair.', + unknown: 'Waiting to validate the connection.' + }[signal.status]; } if (signal.scope === 'capability' || signal.scope === 'bot') { - return { ok: 'Capability requirements are satisfied.', info: 'The capability is disabled or paused.', warning: 'Capability configuration is incomplete.', error: 'The capability is blocked or degraded.', unknown: 'Capability state is unknown.' }[signal.status]; + return { + ok: 'Capability requirements are satisfied.', + info: 'The capability is disabled or paused.', + warning: 'Capability configuration is incomplete.', + error: 'The capability is blocked or degraded.', + unknown: 'Capability state is unknown.' + }[signal.status]; } - return { ok: 'The health check passed.', info: 'Review this health signal.', warning: 'This health signal needs attention.', error: 'This health signal reports an error.', unknown: 'Health state is unknown.' }[signal.status]; + return { + ok: 'The health check passed.', + info: 'Review this health signal.', + warning: 'This health signal needs attention.', + error: 'This health signal reports an error.', + unknown: 'Health state is unknown.' + }[signal.status]; } - function englishSignalDetail(signal: HealthSignal): string | undefined { if (!signal.detail) return undefined; if (signal.scope === 'llm_connection' && signal.layer === 'validation' && signal.status === 'ok') { @@ -185,3 +448,64 @@ function englishSignalDetail(signal: HealthSignal): string | undefined { } return 'See the corresponding settings page for details.'; } + +function koreanSignalMessage(signal: HealthSignal): string { + if (signal.scope === 'llm_connection') { + if (signal.layer === 'configuration') { + if (signal.message === '不是工作区的默认模型来源。') return '작업 공간의 기본 모델 소스가 아닙니다.'; + if (signal.message === '没有启用任何模型。') return '이 연결에서 활성화된 모델이 없습니다.'; + return signal.status === 'info' ? '연결이 비활성화되었습니다.' : '기본 모델을 선택하세요.'; + } + if (signal.layer === 'runtime_probe') { + return { + ok: '최근 전송이 완료되었습니다.', + info: '최근 전송이 사용자에 의해 중지되었습니다.', + warning: '최근 전송이 실패했습니다.', + error: '최근 전송이 실패했습니다.', + unknown: '전송 경로 런타임 확인을 기다리는 중입니다.', + }[signal.status]; + } + return { + ok: '자격 증명 및 엔드포인트 확인을 통과했습니다.', + info: '연결 확인에 주의가 필요합니다.', + warning: '최근 연결 확인에 실패했습니다.', + error: '연결 인증을 복구해야 합니다.', + unknown: '연결 확인을 기다리는 중입니다.', + }[signal.status]; + } + if (signal.scope === 'capability' || signal.scope === 'bot') { + return { + ok: '기능 요구 사항이 충족되었습니다.', + info: '기능이 비활성화되었거나 일시 중지되었습니다.', + warning: '기능 구성이 완료되지 않았습니다.', + error: '기능이 차단되었거나 성능이 저하되었습니다.', + unknown: '기능 상태를 알 수 없습니다.', + }[signal.status]; + } + return { + ok: '건강 확인을 통과했습니다.', + info: '이 건강 신호를 검토하세요.', + warning: '이 건강 신호에 주의가 필요합니다.', + error: '이 건강 신호에서 오류를 보고했습니다.', + unknown: '건강 상태를 알 수 없습니다.', + }[signal.status]; +} + +function koreanSignalDetail(signal: HealthSignal): string | undefined { + if (!signal.detail) return undefined; + if (signal.scope === 'llm_connection' && signal.layer === 'validation' && signal.status === 'ok') { + return '연결만 확인하며 전송, 스트리밍 또는 중단 경로가 성공적으로 실행되었음을 의미하지 않습니다.'; + } + if (signal.scope === 'llm_connection' && signal.layer === 'runtime_probe') { + const model = signal.detail.match(/模型=([^·]+)/u)?.[1]?.trim(); + const latency = signal.detail.match(/延迟=([^·]+)/u)?.[1]?.trim(); + const errorClass = signal.detail.match(/错误类型=([^·]+)/u)?.[1]?.trim(); + const parts = [model && `모델=${model}`, latency && `지연 시간=${latency}`, errorClass && `오류 유형=${errorClass}`].filter(Boolean); + return parts.length > 0 ? parts.join(' · ') : '런타임 세부 정보는 사용량 설정에서 확인할 수 있습니다.'; + } + if (signal.scope === 'llm_connection' && signal.layer === 'configuration') { + if (signal.message === '不是工作区的默认模型来源。') return '이 연결의 모델은 작업에서 명시적으로 선택할 수 있습니다. 새 채팅의 기본 모델은 설정 · 일반에서 지정합니다.'; + if (signal.message === '没有启用任何模型。') return '설정 · 모델의 연결 세부 정보에서 하나 이상의 모델을 활성화하세요.'; + } + return '자세한 내용은 해당 설정 페이지를 확인하세요.'; +} diff --git a/apps/desktop/src/renderer/locales/settings-memory-copy.ts b/apps/desktop/src/renderer/locales/settings-memory-copy.ts index 3c8685a733..477363295a 100644 --- a/apps/desktop/src/renderer/locales/settings-memory-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-memory-copy.ts @@ -18,33 +18,8 @@ */ import type { LocalMemoryState } from '@maka/core/local-memory'; - import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - -type MemoryTextKey = - | 'localFile' | 'localFileHelp' | 'enableLocalFile' | 'agentReadable' | 'agentReadableHelp' | 'enableAgentRead' - | 'waitingFile' | 'waitingBackup' | 'dirty' | 'savedDraft' - | 'backupCandidates' | 'backupCandidatesAria' | 'opening' | 'open' | 'restoring' | 'restore' | 'copying' | 'copyReference' - | 'backupHelp' | 'savedAt' | 'previewPaused' | 'filterAria' | 'filterPlaceholder' | 'clear' | 'filterEmpty' - | 'filterEmptyHelp' | 'activeMemories' | 'archivedMemories' | 'waitingEntry' | 'waitingEntryHelp' | 'manualAddAria' - | 'manualAdd' | 'manualAddHelp' | 'title' | 'titlePlaceholder' | 'tags' | 'tagsPlaceholder' | 'content' - | 'contentPlaceholder' | 'addDraft' | 'sensitiveDraft' | 'sensitiveDraftHelp' | 'fileContent' - | 'fileActionsAria' | 'saving' | 'save' | 'saved' | 'openFile' | 'openFolder' | 'loading' | 'reload' - | 'openPrevious' | 'copyPath' | 'copyPrevious' | 'resetting' | 'resetBackup' | 'restorePrevious' - | 'archiveDraftNotice' | 'noMatchEntry' | 'noEntry' | 'created' | 'updated' | 'archivedNoPrompt' | 'activePrompt' - | 'locateDraft' | 'promptPreview' | 'willInject' | 'willNotInject' - | 'copyContext' | 'promptPreviewHelp' | 'safeModePreview' | 'emptyPromptPreview' - | 'loadFailed' | 'reloaded' | 'reloadDiscarded' | 'toggleFailed' | 'agentReadFailed' | 'saveBlocked' | 'safeMode' - | 'savedRedacted' | 'savedFile' | 'saveFailed' | 'resetDone' | 'resetDoneDetail' | 'resetFailed' | 'noBackup' - | 'noBackupDetail' | 'restoreLatestTitle' | 'restoreCandidateTitle' | 'confirmRestore' | 'cancel' | 'restoredLatest' - | 'restoredCandidate' | 'restoredDetail' | 'restoreFailed' | 'restoreLatestFailed' | 'restoreCandidateFailed' - | 'openFailed' | 'openPreviousFailed' | 'pathCopied' | 'copyFailed' | 'copyFailedDetail' | 'backupReferenceCopied' - | 'entryReferenceCopied' | 'locateFailed' | 'locateFailedDetail' | 'emptyTitle' | 'emptyTitleDetail' | 'emptyContent' - | 'emptyContentDetail' | 'draftOversize' | 'oversizeDetail' | 'addedDraft' | 'addedDraftDetail' | 'updateFailed' - | 'invalidIdDetail' | 'archivedDraft' | 'restoredDraft' | 'updateBlocked' | 'archived' | 'restored' | 'archiveFailed' - | 'entryRestoreFailed' | 'promptCopied' | 'promptCopiedDetail' - | 'restoreDraftAction' | 'archiveDraftAction' | 'restoreAction' | 'archiveAction'; - +type MemoryTextKey = 'localFile' | 'localFileHelp' | 'enableLocalFile' | 'agentReadable' | 'agentReadableHelp' | 'enableAgentRead' | 'waitingFile' | 'waitingBackup' | 'dirty' | 'savedDraft' | 'backupCandidates' | 'backupCandidatesAria' | 'opening' | 'open' | 'restoring' | 'restore' | 'copying' | 'copyReference' | 'backupHelp' | 'savedAt' | 'previewPaused' | 'filterAria' | 'filterPlaceholder' | 'clear' | 'filterEmpty' | 'filterEmptyHelp' | 'activeMemories' | 'archivedMemories' | 'waitingEntry' | 'waitingEntryHelp' | 'manualAddAria' | 'manualAdd' | 'manualAddHelp' | 'title' | 'titlePlaceholder' | 'tags' | 'tagsPlaceholder' | 'content' | 'contentPlaceholder' | 'addDraft' | 'sensitiveDraft' | 'sensitiveDraftHelp' | 'fileContent' | 'fileActionsAria' | 'saving' | 'save' | 'saved' | 'openFile' | 'openFolder' | 'loading' | 'reload' | 'openPrevious' | 'copyPath' | 'copyPrevious' | 'resetting' | 'resetBackup' | 'restorePrevious' | 'archiveDraftNotice' | 'noMatchEntry' | 'noEntry' | 'created' | 'updated' | 'archivedNoPrompt' | 'activePrompt' | 'locateDraft' | 'promptPreview' | 'willInject' | 'willNotInject' | 'copyContext' | 'promptPreviewHelp' | 'safeModePreview' | 'emptyPromptPreview' | 'loadFailed' | 'reloaded' | 'reloadDiscarded' | 'toggleFailed' | 'agentReadFailed' | 'saveBlocked' | 'safeMode' | 'savedRedacted' | 'savedFile' | 'saveFailed' | 'resetDone' | 'resetDoneDetail' | 'resetFailed' | 'noBackup' | 'noBackupDetail' | 'restoreLatestTitle' | 'restoreCandidateTitle' | 'confirmRestore' | 'cancel' | 'restoredLatest' | 'restoredCandidate' | 'restoredDetail' | 'restoreFailed' | 'restoreLatestFailed' | 'restoreCandidateFailed' | 'openFailed' | 'openPreviousFailed' | 'pathCopied' | 'copyFailed' | 'copyFailedDetail' | 'backupReferenceCopied' | 'entryReferenceCopied' | 'locateFailed' | 'locateFailedDetail' | 'emptyTitle' | 'emptyTitleDetail' | 'emptyContent' | 'emptyContentDetail' | 'draftOversize' | 'oversizeDetail' | 'addedDraft' | 'addedDraftDetail' | 'updateFailed' | 'invalidIdDetail' | 'archivedDraft' | 'restoredDraft' | 'updateBlocked' | 'archived' | 'restored' | 'archiveFailed' | 'entryRestoreFailed' | 'promptCopied' | 'promptCopiedDetail' | 'restoreDraftAction' | 'archiveDraftAction' | 'restoreAction' | 'archiveAction'; export type MemorySettingsCopy = { intlLocale: string; text: Record; @@ -52,7 +27,12 @@ export type MemorySettingsCopy = { entryStatuses: Record; backupKinds: Record['kind'], string>; memoryStatuses: Record; - promptBlocked: { disabled: string; incognito: string; safeMode: string; agentRead: string }; + promptBlocked: { + disabled: string; + incognito: string; + safeMode: string; + agentRead: string; + }; countActive(count: number, draft?: boolean): string; countArchived(count: number, draft?: boolean): string; saveSummary(active: number, archived: number): string; @@ -77,48 +57,548 @@ export type MemorySettingsCopy = { previewUsage(length: string, limit: string): string; previewLimit(limit: string): string; }; - const zhText = { - localFile: '本地 MEMORY.md', localFileHelp: '透明 Markdown 文件,保存在当前本机工作区。这里的内容不会自动从聊天里抽取。', enableLocalFile: '启用本地 MEMORY.md', agentReadable: '模型上下文可读取', agentReadableHelp: '默认关闭。开启后,发送消息时会把本地记忆一并提供给模型;隐身模式下仍然不提供。', enableAgentRead: '允许模型上下文读取本地记忆', waitingFile: '等待创建 MEMORY.md', waitingBackup: '等待生成上一版备份', dirty: '有未保存修改', savedDraft: '草稿已保存', backupCandidates: '备份候选', backupCandidatesAria: '本地记忆备份候选列表', opening: '打开中…', open: '打开', restoring: '恢复中…', restore: '恢复', copying: '复制中…', copyReference: '复制引用', backupHelp: '上一版操作会使用最近的候选;这里只显示 metadata,不展示备份正文。', savedAt: '保存于 ', previewPaused: '草稿条目预览暂停', filterAria: '筛选本地记忆', filterPlaceholder: '筛选标题、内容、ID 或标签', clear: '清除', filterEmpty: '没有匹配的记忆条目', filterEmptyHelp: '筛选不会修改 MEMORY.md;清除筛选后会恢复显示全部条目。', activeMemories: '生效记忆', archivedMemories: '已归档记忆', waitingEntry: '等待添加记忆条目', waitingEntryHelp: '在任务里确认记忆,或点右上角「添加记忆」。', manualAddAria: '手动添加本地记忆', manualAdd: '手动添加记忆', manualAddHelp: '填写后立即写入本机 MEMORY.md。', title: '记忆标题', titlePlaceholder: '标题', tags: '记忆标签', tagsPlaceholder: '标签(逗号分隔,可选)', content: '记忆内容', contentPlaceholder: '内容', addDraft: '添加记忆', sensitiveDraft: '草稿含疑似敏感字段', sensitiveDraftHelp: '保存时会先遮蔽疑似 token、API key 或密码,再写入 MEMORY.md。', fileContent: 'MEMORY.md 内容', fileActionsAria: 'MEMORY.md 文件操作', saving: '保存中…', save: '保存', saved: '已保存', openFile: '打开 MEMORY.md', openFolder: '打开所在目录', loading: '载入中…', reload: '重新载入', openPrevious: '打开上一版', copyPath: '复制路径', copyPrevious: '复制上一版引用', resetting: '重置中…', resetBackup: '重置并备份', restorePrevious: '恢复上一版', archiveDraftNotice: '当前归档/恢复操作只更新草稿,保存后才会写入 MEMORY.md。', noMatchEntry: '无匹配条目。', noEntry: '暂无条目。', created: '创建 ', updated: '更新 ', archivedNoPrompt: '已归档,不会提供给模型', activePrompt: '生效条目,发送时会提供给模型', locateDraft: '定位草稿', promptPreview: '模型上下文预览', willInject: '发送时会提供', willNotInject: '当前不会提供', copyContext: '复制上下文', promptPreviewHelp: '这里是发送时会提供给模型的内容;已归档条目不在其中,疑似密钥会遮蔽。', safeModePreview: 'MEMORY.md 过大,当前不会生成模型上下文预览。', emptyPromptPreview: '没有生效记忆会提供给模型。', loadFailed: '载入本地记忆失败', reloaded: '已重新载入 MEMORY.md', reloadDiscarded: '未保存的草稿修改已丢弃。', toggleFailed: '更新本地记忆开关失败', agentReadFailed: '更新模型读取权限失败', saveBlocked: '保存被拦截', safeMode: 'MEMORY.md 内容过大,已进入安全模式。', savedRedacted: '已保存并遮蔽敏感字段', savedFile: '已保存 MEMORY.md', saveFailed: '保存 MEMORY.md 失败', resetDone: '已重置 MEMORY.md', resetDoneDetail: '上一版已保存为备份文件。', resetFailed: '重置 MEMORY.md 失败', noBackup: '没有可恢复备份', noBackupDetail: '保存或重置 MEMORY.md 后才会生成上一版备份。', restoreLatestTitle: '恢复上一版 MEMORY.md?', restoreCandidateTitle: '恢复这个 MEMORY.md 备份?', confirmRestore: '恢复', cancel: '取消', restoredLatest: '已恢复上一版 MEMORY.md', restoredCandidate: '已恢复 MEMORY.md 备份候选', restoredDetail: '恢复前的当前文件已保存为 restore.bak。', restoreFailed: '恢复失败', restoreLatestFailed: '恢复上一版失败', restoreCandidateFailed: '恢复备份失败', openFailed: '打开失败', openPreviousFailed: '打开上一版失败', pathCopied: '已复制路径', copyFailed: '复制失败', copyFailedDetail: '剪贴板不可用或被系统拒绝。', backupReferenceCopied: '已复制上一版引用', entryReferenceCopied: '已复制记忆引用', locateFailed: '无法定位记忆', locateFailedDetail: '当前草稿里找不到这条记忆;请先保存或刷新后重试。', emptyTitle: '标题不能为空', emptyTitleDetail: '给这条记忆起一个短标题。', emptyContent: '内容不能为空', emptyContentDetail: '写下要保留的偏好或事实。', draftOversize: '草稿过大', oversizeDetail: 'MEMORY.md 超出安全上限,请先删减旧内容。', addedDraft: '已添加记忆', addedDraftDetail: '已写入 MEMORY.md。', updateFailed: '无法更新记忆', invalidIdDetail: '这条记忆没有可识别 ID,已停止更新。', archivedDraft: '已在草稿中归档记忆', restoredDraft: '已在草稿中恢复记忆', updateBlocked: '更新被拦截', archived: '已归档记忆', restored: '已恢复记忆', archiveFailed: '归档记忆失败', entryRestoreFailed: '恢复记忆失败', promptCopied: '已复制模型上下文预览', promptCopiedDetail: '使用同一条 prompt 预览和遮蔽路径。', restoreDraftAction: '恢复到草稿', archiveDraftAction: '归档到草稿', restoreAction: '恢复', archiveAction: '归档', + localFile: '本地 MEMORY.md', + localFileHelp: '透明 Markdown 文件,保存在当前本机工作区。这里的内容不会自动从聊天里抽取。', + enableLocalFile: '启用本地 MEMORY.md', + agentReadable: '模型上下文可读取', + agentReadableHelp: '默认关闭。开启后,发送消息时会把本地记忆一并提供给模型;隐身模式下仍然不提供。', + enableAgentRead: '允许模型上下文读取本地记忆', + waitingFile: '等待创建 MEMORY.md', + waitingBackup: '等待生成上一版备份', + dirty: '有未保存修改', + savedDraft: '草稿已保存', + backupCandidates: '备份候选', + backupCandidatesAria: '本地记忆备份候选列表', + opening: '打开中…', + open: '打开', + restoring: '恢复中…', + restore: '恢复', + copying: '复制中…', + copyReference: '复制引用', + backupHelp: '上一版操作会使用最近的候选;这里只显示 metadata,不展示备份正文。', + savedAt: '保存于 ', + previewPaused: '草稿条目预览暂停', + filterAria: '筛选本地记忆', + filterPlaceholder: '筛选标题、内容、ID 或标签', + clear: '清除', + filterEmpty: '没有匹配的记忆条目', + filterEmptyHelp: '筛选不会修改 MEMORY.md;清除筛选后会恢复显示全部条目。', + activeMemories: '生效记忆', + archivedMemories: '已归档记忆', + waitingEntry: '等待添加记忆条目', + waitingEntryHelp: '在任务里确认记忆,或点右上角「添加记忆」。', + manualAddAria: '手动添加本地记忆', + manualAdd: '手动添加记忆', + manualAddHelp: '填写后立即写入本机 MEMORY.md。', + title: '记忆标题', + titlePlaceholder: '标题', + tags: '记忆标签', + tagsPlaceholder: '标签(逗号分隔,可选)', + content: '记忆内容', + contentPlaceholder: '内容', + addDraft: '添加记忆', + sensitiveDraft: '草稿含疑似敏感字段', + sensitiveDraftHelp: '保存时会先遮蔽疑似 token、API key 或密码,再写入 MEMORY.md。', + fileContent: 'MEMORY.md 内容', + fileActionsAria: 'MEMORY.md 文件操作', + saving: '保存中…', + save: '保存', + saved: '已保存', + openFile: '打开 MEMORY.md', + openFolder: '打开所在目录', + loading: '载入中…', + reload: '重新载入', + openPrevious: '打开上一版', + copyPath: '复制路径', + copyPrevious: '复制上一版引用', + resetting: '重置中…', + resetBackup: '重置并备份', + restorePrevious: '恢复上一版', + archiveDraftNotice: '当前归档/恢复操作只更新草稿,保存后才会写入 MEMORY.md。', + noMatchEntry: '无匹配条目。', + noEntry: '暂无条目。', + created: '创建 ', + updated: '更新 ', + archivedNoPrompt: '已归档,不会提供给模型', + activePrompt: '生效条目,发送时会提供给模型', + locateDraft: '定位草稿', + promptPreview: '模型上下文预览', + willInject: '发送时会提供', + willNotInject: '当前不会提供', + copyContext: '复制上下文', + promptPreviewHelp: '这里是发送时会提供给模型的内容;已归档条目不在其中,疑似密钥会遮蔽。', + safeModePreview: 'MEMORY.md 过大,当前不会生成模型上下文预览。', + emptyPromptPreview: '没有生效记忆会提供给模型。', + loadFailed: '载入本地记忆失败', + reloaded: '已重新载入 MEMORY.md', + reloadDiscarded: '未保存的草稿修改已丢弃。', + toggleFailed: '更新本地记忆开关失败', + agentReadFailed: '更新模型读取权限失败', + saveBlocked: '保存被拦截', + safeMode: 'MEMORY.md 内容过大,已进入安全模式。', + savedRedacted: '已保存并遮蔽敏感字段', + savedFile: '已保存 MEMORY.md', + saveFailed: '保存 MEMORY.md 失败', + resetDone: '已重置 MEMORY.md', + resetDoneDetail: '上一版已保存为备份文件。', + resetFailed: '重置 MEMORY.md 失败', + noBackup: '没有可恢复备份', + noBackupDetail: '保存或重置 MEMORY.md 后才会生成上一版备份。', + restoreLatestTitle: '恢复上一版 MEMORY.md?', + restoreCandidateTitle: '恢复这个 MEMORY.md 备份?', + confirmRestore: '恢复', + cancel: '取消', + restoredLatest: '已恢复上一版 MEMORY.md', + restoredCandidate: '已恢复 MEMORY.md 备份候选', + restoredDetail: '恢复前的当前文件已保存为 restore.bak。', + restoreFailed: '恢复失败', + restoreLatestFailed: '恢复上一版失败', + restoreCandidateFailed: '恢复备份失败', + openFailed: '打开失败', + openPreviousFailed: '打开上一版失败', + pathCopied: '已复制路径', + copyFailed: '复制失败', + copyFailedDetail: '剪贴板不可用或被系统拒绝。', + backupReferenceCopied: '已复制上一版引用', + entryReferenceCopied: '已复制记忆引用', + locateFailed: '无法定位记忆', + locateFailedDetail: '当前草稿里找不到这条记忆;请先保存或刷新后重试。', + emptyTitle: '标题不能为空', + emptyTitleDetail: '给这条记忆起一个短标题。', + emptyContent: '内容不能为空', + emptyContentDetail: '写下要保留的偏好或事实。', + draftOversize: '草稿过大', + oversizeDetail: 'MEMORY.md 超出安全上限,请先删减旧内容。', + addedDraft: '已添加记忆', + addedDraftDetail: '已写入 MEMORY.md。', + updateFailed: '无法更新记忆', + invalidIdDetail: '这条记忆没有可识别 ID,已停止更新。', + archivedDraft: '已在草稿中归档记忆', + restoredDraft: '已在草稿中恢复记忆', + updateBlocked: '更新被拦截', + archived: '已归档记忆', + restored: '已恢复记忆', + archiveFailed: '归档记忆失败', + entryRestoreFailed: '恢复记忆失败', + promptCopied: '已复制模型上下文预览', + promptCopiedDetail: '使用同一条 prompt 预览和遮蔽路径。', + restoreDraftAction: '恢复到草稿', + archiveDraftAction: '归档到草稿', + restoreAction: '恢复', + archiveAction: '归档' } satisfies Record; - const enText = { - localFile: 'Local MEMORY.md', localFileHelp: 'A transparent Markdown file stored in the current local workspace. Content is never extracted from chats automatically.', enableLocalFile: 'Enable local MEMORY.md', agentReadable: 'Available to model context', agentReadableHelp: 'Off by default. When enabled, local memory is given to the model along with your message; incognito mode still withholds it.', enableAgentRead: 'Allow model context to read local memory', waitingFile: 'Waiting to create MEMORY.md', waitingBackup: 'Waiting to create a previous-version backup', dirty: 'Unsaved changes', savedDraft: 'Draft saved', backupCandidates: 'Backup candidates', backupCandidatesAria: 'Local memory backup candidates', opening: 'Opening…', open: 'Open', restoring: 'Restoring…', restore: 'Restore', copying: 'Copying…', copyReference: 'Copy reference', backupHelp: 'Previous-version actions use the latest candidate. Only metadata is shown here, never backup contents.', savedAt: 'Saved ', previewPaused: 'Draft entry preview paused', filterAria: 'Filter local memory', filterPlaceholder: 'Filter title, content, ID, or tags', clear: 'Clear', filterEmpty: 'No matching memory entries', filterEmptyHelp: 'Filtering does not modify MEMORY.md. Clear the filter to show all entries.', activeMemories: 'Active memories', archivedMemories: 'Archived memories', waitingEntry: 'Ready to add a memory entry', waitingEntryHelp: 'Confirm a memory in chat, or use "Add memory" above.', manualAddAria: 'Add local memory manually', manualAdd: 'Add memory manually', manualAddHelp: 'Written to the local MEMORY.md immediately.', title: 'Memory title', titlePlaceholder: 'Title', tags: 'Memory tags', tagsPlaceholder: 'Tags (comma-separated, optional)', content: 'Memory content', contentPlaceholder: 'Content', addDraft: 'Add memory', sensitiveDraft: 'Draft may contain sensitive fields', sensitiveDraftHelp: 'Suspected tokens, API keys, and passwords are redacted before MEMORY.md is written.', fileContent: 'MEMORY.md content', fileActionsAria: 'MEMORY.md file actions', saving: 'Saving…', save: 'Save', saved: 'Saved', openFile: 'Open MEMORY.md', openFolder: 'Open containing folder', loading: 'Loading…', reload: 'Reload', openPrevious: 'Open previous version', copyPath: 'Copy path', copyPrevious: 'Copy previous-version reference', resetting: 'Resetting…', resetBackup: 'Reset and back up', restorePrevious: 'Restore previous version', archiveDraftNotice: 'Archive and restore actions update only the draft until you save MEMORY.md.', noMatchEntry: 'No matching entries.', noEntry: 'No entries yet.', created: 'Created ', updated: 'Updated ', archivedNoPrompt: 'Archived; not given to the model', activePrompt: 'Active entry; given to the model when you send', locateDraft: 'Locate in draft', promptPreview: 'Model context preview', willInject: 'Included when sending', willNotInject: 'Not currently included', copyContext: 'Copy context', promptPreviewHelp: 'What will be given to the model when you send. Archived entries are excluded and suspected secrets are redacted.', safeModePreview: 'MEMORY.md is too large, so no model-context preview is generated.', emptyPromptPreview: 'No active memories will be given to the model.', loadFailed: 'Failed to load local memory', reloaded: 'MEMORY.md reloaded', reloadDiscarded: 'Unsaved draft changes were discarded.', toggleFailed: 'Failed to update local memory', agentReadFailed: 'Failed to update model read access', saveBlocked: 'Save blocked', safeMode: 'MEMORY.md is too large and entered safe mode.', savedRedacted: 'Saved with sensitive fields redacted', savedFile: 'MEMORY.md saved', saveFailed: 'Failed to save MEMORY.md', resetDone: 'MEMORY.md reset', resetDoneDetail: 'The previous version was saved as a backup.', resetFailed: 'Failed to reset MEMORY.md', noBackup: 'No backup available to restore', noBackupDetail: 'A previous-version backup is created after you save or reset MEMORY.md.', restoreLatestTitle: 'Restore the previous MEMORY.md version?', restoreCandidateTitle: 'Restore this MEMORY.md backup?', confirmRestore: 'Restore', cancel: 'Cancel', restoredLatest: 'Previous MEMORY.md version restored', restoredCandidate: 'MEMORY.md backup candidate restored', restoredDetail: 'The file from before the restore was saved as restore.bak.', restoreFailed: 'Restore failed', restoreLatestFailed: 'Failed to restore previous version', restoreCandidateFailed: 'Failed to restore backup', openFailed: 'Open failed', openPreviousFailed: 'Failed to open previous version', pathCopied: 'Path copied', copyFailed: 'Copy failed', copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', backupReferenceCopied: 'Previous-version reference copied', entryReferenceCopied: 'Memory reference copied', locateFailed: 'Could not locate memory', locateFailedDetail: 'This memory is not in the current draft. Save or reload, then try again.', emptyTitle: 'Title is required', emptyTitleDetail: 'Give this memory a short title.', emptyContent: 'Content is required', emptyContentDetail: 'Enter the preference or fact to retain.', draftOversize: 'Draft is too large', oversizeDetail: 'MEMORY.md exceeds the safety limit. Remove older content first.', addedDraft: 'Memory added', addedDraftDetail: 'Written to MEMORY.md.', updateFailed: 'Could not update memory', invalidIdDetail: 'This memory has no recognizable ID, so the update was stopped.', archivedDraft: 'Memory archived in draft', restoredDraft: 'Memory restored in draft', updateBlocked: 'Update blocked', archived: 'Memory archived', restored: 'Memory restored', archiveFailed: 'Failed to archive memory', entryRestoreFailed: 'Failed to restore memory', promptCopied: 'Model context preview copied', promptCopiedDetail: 'Uses the same prompt-preview and redaction path.', restoreDraftAction: 'Restore to draft', archiveDraftAction: 'Archive in draft', restoreAction: 'Restore', archiveAction: 'Archive', + localFile: 'Local MEMORY.md', + localFileHelp: 'A transparent Markdown file stored in the current local workspace. Content is never extracted from chats automatically.', + enableLocalFile: 'Enable local MEMORY.md', + agentReadable: 'Available to model context', + agentReadableHelp: 'Off by default. When enabled, local memory is given to the model along with your message; incognito mode still withholds it.', + enableAgentRead: 'Allow model context to read local memory', + waitingFile: 'Waiting to create MEMORY.md', + waitingBackup: 'Waiting to create a previous-version backup', + dirty: 'Unsaved changes', + savedDraft: 'Draft saved', + backupCandidates: 'Backup candidates', + backupCandidatesAria: 'Local memory backup candidates', + opening: 'Opening…', + open: 'Open', + restoring: 'Restoring…', + restore: 'Restore', + copying: 'Copying…', + copyReference: 'Copy reference', + backupHelp: 'Previous-version actions use the latest candidate. Only metadata is shown here, never backup contents.', + savedAt: 'Saved ', + previewPaused: 'Draft entry preview paused', + filterAria: 'Filter local memory', + filterPlaceholder: 'Filter title, content, ID, or tags', + clear: 'Clear', + filterEmpty: 'No matching memory entries', + filterEmptyHelp: 'Filtering does not modify MEMORY.md. Clear the filter to show all entries.', + activeMemories: 'Active memories', + archivedMemories: 'Archived memories', + waitingEntry: 'Ready to add a memory entry', + waitingEntryHelp: 'Confirm a memory in chat, or use "Add memory" above.', + manualAddAria: 'Add local memory manually', + manualAdd: 'Add memory manually', + manualAddHelp: 'Written to the local MEMORY.md immediately.', + title: 'Memory title', + titlePlaceholder: 'Title', + tags: 'Memory tags', + tagsPlaceholder: 'Tags (comma-separated, optional)', + content: 'Memory content', + contentPlaceholder: 'Content', + addDraft: 'Add memory', + sensitiveDraft: 'Draft may contain sensitive fields', + sensitiveDraftHelp: 'Suspected tokens, API keys, and passwords are redacted before MEMORY.md is written.', + fileContent: 'MEMORY.md content', + fileActionsAria: 'MEMORY.md file actions', + saving: 'Saving…', + save: 'Save', + saved: 'Saved', + openFile: 'Open MEMORY.md', + openFolder: 'Open containing folder', + loading: 'Loading…', + reload: 'Reload', + openPrevious: 'Open previous version', + copyPath: 'Copy path', + copyPrevious: 'Copy previous-version reference', + resetting: 'Resetting…', + resetBackup: 'Reset and back up', + restorePrevious: 'Restore previous version', + archiveDraftNotice: 'Archive and restore actions update only the draft until you save MEMORY.md.', + noMatchEntry: 'No matching entries.', + noEntry: 'No entries yet.', + created: 'Created ', + updated: 'Updated ', + archivedNoPrompt: 'Archived; not given to the model', + activePrompt: 'Active entry; given to the model when you send', + locateDraft: 'Locate in draft', + promptPreview: 'Model context preview', + willInject: 'Included when sending', + willNotInject: 'Not currently included', + copyContext: 'Copy context', + promptPreviewHelp: 'What will be given to the model when you send. Archived entries are excluded and suspected secrets are redacted.', + safeModePreview: 'MEMORY.md is too large, so no model-context preview is generated.', + emptyPromptPreview: 'No active memories will be given to the model.', + loadFailed: 'Failed to load local memory', + reloaded: 'MEMORY.md reloaded', + reloadDiscarded: 'Unsaved draft changes were discarded.', + toggleFailed: 'Failed to update local memory', + agentReadFailed: 'Failed to update model read access', + saveBlocked: 'Save blocked', + safeMode: 'MEMORY.md is too large and entered safe mode.', + savedRedacted: 'Saved with sensitive fields redacted', + savedFile: 'MEMORY.md saved', + saveFailed: 'Failed to save MEMORY.md', + resetDone: 'MEMORY.md reset', + resetDoneDetail: 'The previous version was saved as a backup.', + resetFailed: 'Failed to reset MEMORY.md', + noBackup: 'No backup available to restore', + noBackupDetail: 'A previous-version backup is created after you save or reset MEMORY.md.', + restoreLatestTitle: 'Restore the previous MEMORY.md version?', + restoreCandidateTitle: 'Restore this MEMORY.md backup?', + confirmRestore: 'Restore', + cancel: 'Cancel', + restoredLatest: 'Previous MEMORY.md version restored', + restoredCandidate: 'MEMORY.md backup candidate restored', + restoredDetail: 'The file from before the restore was saved as restore.bak.', + restoreFailed: 'Restore failed', + restoreLatestFailed: 'Failed to restore previous version', + restoreCandidateFailed: 'Failed to restore backup', + openFailed: 'Open failed', + openPreviousFailed: 'Failed to open previous version', + pathCopied: 'Path copied', + copyFailed: 'Copy failed', + copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', + backupReferenceCopied: 'Previous-version reference copied', + entryReferenceCopied: 'Memory reference copied', + locateFailed: 'Could not locate memory', + locateFailedDetail: 'This memory is not in the current draft. Save or reload, then try again.', + emptyTitle: 'Title is required', + emptyTitleDetail: 'Give this memory a short title.', + emptyContent: 'Content is required', + emptyContentDetail: 'Enter the preference or fact to retain.', + draftOversize: 'Draft is too large', + oversizeDetail: 'MEMORY.md exceeds the safety limit. Remove older content first.', + addedDraft: 'Memory added', + addedDraftDetail: 'Written to MEMORY.md.', + updateFailed: 'Could not update memory', + invalidIdDetail: 'This memory has no recognizable ID, so the update was stopped.', + archivedDraft: 'Memory archived in draft', + restoredDraft: 'Memory restored in draft', + updateBlocked: 'Update blocked', + archived: 'Memory archived', + restored: 'Memory restored', + archiveFailed: 'Failed to archive memory', + entryRestoreFailed: 'Failed to restore memory', + promptCopied: 'Model context preview copied', + promptCopiedDetail: 'Uses the same prompt-preview and redaction path.', + restoreDraftAction: 'Restore to draft', + archiveDraftAction: 'Archive in draft', + restoreAction: 'Restore', + archiveAction: 'Archive' } satisfies Record; - -const SETTINGS_MEMORY_COPY = { +const SETTINGS_MEMORY_COPY_BASE = { zh: makeCopy('zh-CN', zhText, { - origins: { manual: '手动记录', imported: '导入记录', extracted: '确认提取', unknown: '手写条目' }, entryStatuses: { draft: '草稿', review_required: '待确认', active: '生效', archived: '已归档', rejected: '已拒绝', unknown: '未识别' }, backupKinds: { reset: '重置前备份', restore: '恢复前备份', save: '保存前备份' }, memoryStatuses: { ok: '本地文件已就绪', disabled: '已关闭', safe_mode: '安全模式', incognito_blocked: '隐身禁用', error: '读取失败' }, promptBlocked: { disabled: '本地记忆已关闭。', incognito: '隐身模式下不会提供本地记忆。', safeMode: 'MEMORY.md 过大,当前不会提供。', agentRead: '模型上下文读取未开启。' }, backupOversize: '备份过大,无法预览条目', previewOversize: '草稿过大,条目预览已暂停;保存前请先删减 MEMORY.md 内容。', previewTruncationMarker: '[本地记忆已按长度截断]', + origins: { + manual: '手动记录', + imported: '导入记录', + extracted: '确认提取', + unknown: '手写条目' + }, + entryStatuses: { + draft: '草稿', + review_required: '待确认', + active: '生效', + archived: '已归档', + rejected: '已拒绝', + unknown: '未识别' + }, + backupKinds: { + reset: '重置前备份', + restore: '恢复前备份', + save: '保存前备份' + }, + memoryStatuses: { + ok: '本地文件已就绪', + disabled: '已关闭', + safe_mode: '安全模式', + incognito_blocked: '隐身禁用', + error: '读取失败' + }, + promptBlocked: { + disabled: '本地记忆已关闭。', + incognito: '隐身模式下不会提供本地记忆。', + safeMode: 'MEMORY.md 过大,当前不会提供。', + agentRead: '模型上下文读取未开启。' + }, + backupOversize: '备份过大,无法预览条目', + previewOversize: '草稿过大,条目预览已暂停;保存前请先删减 MEMORY.md 内容。', + previewTruncationMarker: '[本地记忆已按长度截断]' }), en: makeCopy('en-US', enText, { - origins: { manual: 'Manual entry', imported: 'Imported entry', extracted: 'Confirmed extraction', unknown: 'Handwritten entry' }, entryStatuses: { draft: 'Draft', review_required: 'Needs review', active: 'Active', archived: 'Archived', rejected: 'Rejected', unknown: 'Unrecognized' }, backupKinds: { reset: 'Before reset', restore: 'Before restore', save: 'Before save' }, memoryStatuses: { ok: 'Local file ready', disabled: 'Off', safe_mode: 'Safe mode', incognito_blocked: 'Disabled in incognito', error: 'Read failed' }, promptBlocked: { disabled: 'Local memory is disabled.', incognito: 'Local memory is never added in incognito mode.', safeMode: 'MEMORY.md is too large and will not be added.', agentRead: 'Model context access is disabled.' }, backupOversize: 'Backup is too large to preview entries', previewOversize: 'The draft is too large, so entry preview is paused. Reduce MEMORY.md before saving.', previewTruncationMarker: '[Local memory truncated to the length limit]', - }), + origins: { + manual: 'Manual entry', + imported: 'Imported entry', + extracted: 'Confirmed extraction', + unknown: 'Handwritten entry' + }, + entryStatuses: { + draft: 'Draft', + review_required: 'Needs review', + active: 'Active', + archived: 'Archived', + rejected: 'Rejected', + unknown: 'Unrecognized' + }, + backupKinds: { + reset: 'Before reset', + restore: 'Before restore', + save: 'Before save' + }, + memoryStatuses: { + ok: 'Local file ready', + disabled: 'Off', + safe_mode: 'Safe mode', + incognito_blocked: 'Disabled in incognito', + error: 'Read failed' + }, + promptBlocked: { + disabled: 'Local memory is disabled.', + incognito: 'Local memory is never added in incognito mode.', + safeMode: 'MEMORY.md is too large and will not be added.', + agentRead: 'Model context access is disabled.' + }, + backupOversize: 'Backup is too large to preview entries', + previewOversize: 'The draft is too large, so entry preview is paused. Reduce MEMORY.md before saving.', + previewTruncationMarker: '[Local memory truncated to the length limit]' + }) +}; +const koText = { + localFile: '로컬 MEMORY.md', + localFileHelp: '현재 로컬 작업 공간에 저장된 투명한 Markdown 파일입니다. 채팅에서 내용이 자동으로 추출되지 않습니다.', + enableLocalFile: '로컬 MEMORY.md 활성화', + agentReadable: '모델 컨텍스트에서 사용 가능', + agentReadableHelp: '기본값은 꺼져 있습니다. 켜면 메시지와 함께 로컬 메모리가 모델에 제공되며, 시크릿 모드에서는 계속 제외됩니다.', + enableAgentRead: '모델 컨텍스트의 로컬 메모리 읽기 허용', + waitingFile: 'MEMORY.md 생성 대기 중', + waitingBackup: '이전 버전 백업 생성 대기 중', + dirty: '저장되지 않은 변경 사항', + savedDraft: '초안 저장됨', + backupCandidates: '백업 후보', + backupCandidatesAria: '로컬 메모리 백업 후보', + opening: '여는 중…', + open: '열기', + restoring: '복원 중…', + restore: '복원', + copying: '복사 중…', + copyReference: '참조 복사', + backupHelp: '이전 버전 작업은 최신 후보를 사용합니다. 여기에는 메타데이터만 표시하고 백업 내용은 표시하지 않습니다.', + savedAt: '저장 시간 ', + previewPaused: '초안 항목 미리보기 일시 중지됨', + filterAria: '로컬 메모리 필터', + filterPlaceholder: '제목, 내용, ID 또는 태그 필터', + clear: '지우기', + filterEmpty: '일치하는 메모리 항목이 없습니다', + filterEmptyHelp: '필터링은 MEMORY.md를 수정하지 않습니다. 필터를 지우면 모든 항목이 표시됩니다.', + activeMemories: '활성 메모리', + archivedMemories: '보관된 메모리', + waitingEntry: '메모리 항목을 추가할 준비가 되었습니다', + waitingEntryHelp: '채팅에서 메모리를 확인하거나 위의 “메모리 추가”를 사용하세요.', + manualAddAria: '로컬 메모리 수동 추가', + manualAdd: '메모리 수동 추가', + manualAddHelp: '로컬 MEMORY.md에 즉시 기록됩니다.', + title: '메모리 제목', + titlePlaceholder: '제목', + tags: '메모리 태그', + tagsPlaceholder: '태그(쉼표로 구분, 선택 사항)', + content: '메모리 내용', + contentPlaceholder: '내용', + addDraft: '메모리 추가', + sensitiveDraft: '초안에 민감한 필드가 포함될 수 있음', + sensitiveDraftHelp: 'MEMORY.md에 기록하기 전에 의심되는 토큰, API 키 및 비밀번호가 가려집니다.', + fileContent: 'MEMORY.md 내용', + fileActionsAria: 'MEMORY.md 파일 작업', + saving: '저장 중…', + save: '저장', + saved: '저장됨', + openFile: 'MEMORY.md 열기', + openFolder: '포함된 폴더 열기', + loading: '로드 중…', + reload: '새로 고침', + openPrevious: '이전 버전 열기', + copyPath: '경로 복사', + copyPrevious: '이전 버전 참조 복사', + resetting: '초기화 중…', + resetBackup: '초기화 및 백업', + restorePrevious: '이전 버전 복원', + archiveDraftNotice: '보관 및 복원 작업은 MEMORY.md를 저장할 때까지 초안만 업데이트합니다.', + noMatchEntry: '일치하는 항목이 없습니다.', + noEntry: '아직 항목이 없습니다.', + created: '생성 시간 ', + updated: '업데이트 시간 ', + archivedNoPrompt: '보관됨; 모델에 제공되지 않음', + activePrompt: '활성 항목; 전송할 때 모델에 제공됨', + locateDraft: '초안에서 찾기', + promptPreview: '모델 컨텍스트 미리보기', + willInject: '전송 시 포함됨', + willNotInject: '현재 포함되지 않음', + copyContext: '컨텍스트 복사', + promptPreviewHelp: '전송할 때 모델에 제공되는 내용입니다. 보관된 항목은 제외되고 의심되는 비밀 정보는 가려집니다.', + safeModePreview: 'MEMORY.md가 너무 커서 모델 컨텍스트 미리보기를 생성하지 않습니다.', + emptyPromptPreview: '활성 메모리가 모델에 제공되지 않습니다.', + loadFailed: '로컬 메모리를 로드하지 못했습니다', + reloaded: 'MEMORY.md를 다시 로드했습니다', + reloadDiscarded: '저장하지 않은 초안 변경 사항을 버렸습니다.', + toggleFailed: '로컬 메모리를 업데이트하지 못했습니다', + agentReadFailed: '모델 읽기 권한을 업데이트하지 못했습니다', + saveBlocked: '저장이 차단됨', + safeMode: 'MEMORY.md가 너무 커서 안전 모드로 전환되었습니다.', + savedRedacted: '민감한 필드를 가리고 저장됨', + savedFile: 'MEMORY.md 저장됨', + saveFailed: 'MEMORY.md를 저장하지 못했습니다', + resetDone: 'MEMORY.md 초기화됨', + resetDoneDetail: '이전 버전이 백업 파일로 저장되었습니다.', + resetFailed: 'MEMORY.md를 초기화하지 못했습니다', + noBackup: '복원할 수 있는 백업이 없습니다', + noBackupDetail: 'MEMORY.md를 저장하거나 초기화한 후 이전 버전 백업이 생성됩니다.', + restoreLatestTitle: '이전 MEMORY.md 버전을 복원하시겠습니까?', + restoreCandidateTitle: '이 MEMORY.md 백업을 복원하시겠습니까?', + confirmRestore: '복원', + cancel: '취소', + restoredLatest: '이전 MEMORY.md 버전이 복원됨', + restoredCandidate: 'MEMORY.md 백업 후보가 복원됨', + restoredDetail: '복원 전 파일은 restore.bak으로 저장되었습니다.', + restoreFailed: '복원하지 못했습니다', + restoreLatestFailed: '이전 버전을 복원하지 못했습니다', + restoreCandidateFailed: '백업을 복원하지 못했습니다', + openFailed: '열지 못했습니다', + openPreviousFailed: '이전 버전을 열지 못했습니다', + pathCopied: '경로 복사됨', + copyFailed: '복사하지 못했습니다', + copyFailedDetail: '클립보드를 사용할 수 없거나 시스템에서 액세스가 거부되었습니다.', + backupReferenceCopied: '이전 버전 참조 복사됨', + entryReferenceCopied: '메모리 참조 복사됨', + locateFailed: '메모리를 찾지 못했습니다', + locateFailedDetail: '현재 초안에 이 메모리가 없습니다. 저장하거나 다시 로드한 후 시도하세요.', + emptyTitle: '제목이 필요합니다', + emptyTitleDetail: '이 메모리에 짧은 제목을 지정하세요.', + emptyContent: '내용이 필요합니다', + emptyContentDetail: '보관할 선호 사항이나 사실을 입력하세요.', + draftOversize: '초안이 너무 큽니다', + oversizeDetail: 'MEMORY.md가 안전 한도를 초과했습니다. 먼저 오래된 내용을 삭제하세요.', + addedDraft: '메모리 추가됨', + addedDraftDetail: 'MEMORY.md에 기록되었습니다.', + updateFailed: '메모리를 업데이트하지 못했습니다', + invalidIdDetail: '이 메모리에 인식 가능한 ID가 없어 업데이트를 중지했습니다.', + archivedDraft: '초안에서 메모리 보관됨', + restoredDraft: '초안에서 메모리 복원됨', + updateBlocked: '업데이트가 차단됨', + archived: '메모리 보관됨', + restored: '메모리 복원됨', + archiveFailed: '메모리를 보관하지 못했습니다', + entryRestoreFailed: '메모리를 복원하지 못했습니다', + promptCopied: '모델 컨텍스트 미리보기 복사됨', + promptCopiedDetail: '동일한 프롬프트 미리보기 및 비밀 정보 제거 경로를 사용합니다.', + restoreDraftAction: '초안으로 복원', + archiveDraftAction: '초안에서 보관', + restoreAction: '복원', + archiveAction: '보관' +} satisfies Record; +const SETTINGS_MEMORY_COPY = { + ...SETTINGS_MEMORY_COPY_BASE, + ko: { + ...makeCopy('ko-KR', koText, { + origins: { + manual: '수동 입력', + imported: '가져온 항목', + extracted: '추출 확인됨', + unknown: '직접 작성한 항목' + }, + entryStatuses: { + draft: '초안', + review_required: '검토 필요', + active: '활성', + archived: '보관됨', + rejected: '거부됨', + unknown: '인식할 수 없음' + }, + backupKinds: { + reset: '초기화 전', + restore: '복원 전', + save: '저장 전' + }, + memoryStatuses: { + ok: '로컬 파일 준비됨', + disabled: '꺼짐', + safe_mode: '안전 모드', + incognito_blocked: '시크릿 모드에서 비활성화됨', + error: '읽기 실패' + }, + promptBlocked: { + disabled: '로컬 메모리가 비활성화되었습니다.', + incognito: '시크릿 모드에서는 로컬 메모리가 추가되지 않습니다.', + safeMode: 'MEMORY.md가 너무 커서 추가되지 않습니다.', + agentRead: '모델 컨텍스트 액세스가 비활성화되었습니다.' + }, + backupOversize: '백업이 너무 커서 항목을 미리 볼 수 없습니다', + previewOversize: '초안이 너무 커서 항목 미리보기를 일시 중지했습니다. 저장하기 전에 MEMORY.md를 줄이세요.', + previewTruncationMarker: '[로컬 메모리가 길이 제한에 맞게 잘림]' + }), + intlLocale: 'ko-KR' + } } satisfies UiCatalog; - -export function getMemorySettingsCopy(locale: UiLocale): MemorySettingsCopy { return SETTINGS_MEMORY_COPY[locale]; } - +export function getMemorySettingsCopy(locale: UiLocale): MemorySettingsCopy { + return SETTINGS_MEMORY_COPY[locale]; +} function makeCopy(intlLocale: string, text: Record, values: Pick): MemorySettingsCopy { const plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`; const isZh = intlLocale === 'zh-CN'; + const isKo = intlLocale === 'ko-KR'; return { - intlLocale, text, ...values, - countActive: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} 条生效` : `${draft ? 'Draft · ' : ''}${plural(count, 'active entry', 'active entries')}`, - countArchived: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} 条已归档` : `${draft ? 'Draft · ' : ''}${plural(count, 'archived entry', 'archived entries')}`, - saveSummary: (active, archived) => isZh ? `当前 ${active} 条生效${archived > 0 ? ` / ${archived} 条已归档` : ''};已保留上一版备份。` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}; the previous version was backed up.`, - backupSummary: (active, archived) => isZh ? `${active} 条生效${archived > 0 ? ` / ${archived} 条已归档` : ''}` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}`, - countEntries: (count) => isZh ? `${count} 条记忆` : plural(count, 'memory', 'memories'), - countMatches: (filtered, total) => isZh ? `${filtered} / ${total} 条匹配` : `${filtered} / ${total} matching`, - listAria: (title) => isZh ? `${title}列表` : `${title} list`, - entryActionsAria: (title) => isZh ? `${title} 记忆操作` : `${title} memory actions`, - entryActionAria: (action, identity) => isZh ? `${action}:${identity}` : `${action}: ${identity}`, - openBackupAria: (label) => isZh ? `打开备份候选 ${label}` : `Open backup candidate ${label}`, restoreBackupAria: (label) => isZh ? `恢复备份候选 ${label}` : `Restore backup candidate ${label}`, copyBackupAria: (label) => isZh ? `复制备份候选引用 ${label}` : `Copy backup candidate reference ${label}`, - draftStatusAria: (action) => isZh ? `${action},保存前不会写入 MEMORY.md` : `${action}; MEMORY.md is not written until you save`, - restoreLatestDescription: (label) => isZh ? `会先备份当前 MEMORY.md,再用最近一次备份覆盖当前文件。将恢复:${label}` : `The current MEMORY.md will be backed up before the latest backup replaces it. Restore: ${label}`, - restoreCandidateDescription: (label) => isZh ? `会先备份当前 MEMORY.md,再用选中的备份覆盖当前文件。将恢复:${label}` : `The current MEMORY.md will be backed up before the selected backup replaces it. Restore: ${label}`, - redactedDetail: (summary) => isZh ? `写入前已替换疑似 token、API key 或密码;${summary}` : `Suspected tokens, API keys, or passwords were redacted before writing; ${summary}`, - openBackupFailed: (kind) => isZh ? `打开${kind}失败` : `Failed to open ${kind}`, - previewTruncated: (limit) => isZh ? `预览已按 ${limit} 字符上限截断` : `Preview truncated at the ${limit}-character limit`, - previewUsage: (length, limit) => isZh ? `预览 ${length} / ${limit} 字符` : `Preview ${length} / ${limit} characters`, - previewLimit: (limit) => isZh ? `prompt 上限 ${limit} 字符` : `Prompt limit: ${limit} characters`, + intlLocale, + text, + ...values, + countActive: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} 条生效` : isKo ? `${draft ? '초안 · ' : ''}${count}개 활성 항목` : `${draft ? 'Draft · ' : ''}${plural(count, 'active entry', 'active entries')}`, + countArchived: (count, draft) => isZh ? `${draft ? '草稿 ' : ''}${count} 条已归档` : isKo ? `${draft ? '초안 · ' : ''}${count}개 보관 항목` : `${draft ? 'Draft · ' : ''}${plural(count, 'archived entry', 'archived entries')}`, + saveSummary: (active, archived) => isZh ? `当前 ${active} 条生效${archived > 0 ? ` / ${archived} 条已归档` : ''};已保留上一版备份。` : isKo ? `활성 ${active}개${archived > 0 ? ` / 보관 ${archived}개` : ''}; 이전 버전이 백업되었습니다.` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}; the previous version was backed up.`, + backupSummary: (active, archived) => isZh ? `${active} 条生效${archived > 0 ? ` / ${archived} 条已归档` : ''}` : isKo ? `활성 ${active}개${archived > 0 ? ` / 보관 ${archived}개` : ''}` : `${plural(active, 'active entry', 'active entries')}${archived > 0 ? ` / ${plural(archived, 'archived entry', 'archived entries')}` : ''}`, + countEntries: count => isZh ? `${count} 条记忆` : isKo ? `${count}개 메모리` : plural(count, 'memory', 'memories'), + countMatches: (filtered, total) => isZh ? `${filtered} / ${total} 条匹配` : isKo ? `${filtered} / ${total}개 일치` : `${filtered} / ${total} matching`, + listAria: title => isZh ? `${title}列表` : isKo ? `${title} 목록` : `${title} list`, + entryActionsAria: title => isZh ? `${title} 记忆操作` : isKo ? `${title} 메모리 작업` : `${title} memory actions`, + entryActionAria: (action, identity) => isZh ? `${action}:${identity}` : isKo ? `${action}: ${identity}` : `${action}: ${identity}`, + openBackupAria: label => isZh ? `打开备份候选 ${label}` : isKo ? `백업 후보 ${label} 열기` : `Open backup candidate ${label}`, + restoreBackupAria: label => isZh ? `恢复备份候选 ${label}` : isKo ? `백업 후보 ${label} 복원` : `Restore backup candidate ${label}`, + copyBackupAria: label => isZh ? `复制备份候选引用 ${label}` : isKo ? `백업 후보 참조 ${label} 복사` : `Copy backup candidate reference ${label}`, + draftStatusAria: action => isZh ? `${action},保存前不会写入 MEMORY.md` : isKo ? `${action}; 저장하기 전에는 MEMORY.md에 기록되지 않습니다` : `${action}; MEMORY.md is not written until you save`, + restoreLatestDescription: label => isZh ? `会先备份当前 MEMORY.md,再用最近一次备份覆盖当前文件。将恢复:${label}` : isKo ? `현재 MEMORY.md를 먼저 백업한 후 최신 백업으로 덮어씁니다. 복원 항목: ${label}` : `The current MEMORY.md will be backed up before the latest backup replaces it. Restore: ${label}`, + restoreCandidateDescription: label => isZh ? `会先备份当前 MEMORY.md,再用选中的备份覆盖当前文件。将恢复:${label}` : isKo ? `현재 MEMORY.md를 먼저 백업한 후 선택한 백업으로 덮어씁니다. 복원 항목: ${label}` : `The current MEMORY.md will be backed up before the selected backup replaces it. Restore: ${label}`, + redactedDetail: summary => isZh ? `写入前已替换疑似 token、API key 或密码;${summary}` : isKo ? `기록 전에 의심스러운 token, API 키 또는 비밀번호를 가렸습니다; ${summary}` : `Suspected tokens, API keys, or passwords were redacted before writing; ${summary}`, + openBackupFailed: kind => isZh ? `打开${kind}失败` : isKo ? `${kind}을(를) 열 수 없습니다` : `Failed to open ${kind}`, + previewTruncated: limit => isZh ? `预览已按 ${limit} 字符上限截断` : isKo ? `미리보기가 ${limit}자 제한으로 잘렸습니다` : `Preview truncated at the ${limit}-character limit`, + previewUsage: (length, limit) => isZh ? `预览 ${length} / ${limit} 字符` : isKo ? `미리보기 ${length} / ${limit}자` : `Preview ${length} / ${limit} characters`, + previewLimit: limit => isZh ? `prompt 上限 ${limit} 字符` : isKo ? `프롬프트 제한: ${limit}자` : `Prompt limit: ${limit} characters` }; } diff --git a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts index cd81e72bf8..d038121003 100644 --- a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts @@ -20,67 +20,240 @@ import type { SettingsSection } from '@maka/core/settings'; import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; import type { SettingsNavGroup } from '../settings/nav-group-summary.js'; - export type SettingsNavigationCopy = { groups: Record; - sections: Record; + sections: Record; }; - -const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { +const SETTINGS_NAVIGATION_COPY_BY_LOCALE_BASE = { zh: { groups: { preferences: '偏好', capabilities: '能力', activity: '活动', - system: '系统', + system: '系统' }, sections: { - general: { label: '通用', description: '显示名称与界面语言、隐私与通知、任务默认与网络代理。' }, - appearance: { label: '外观', description: '界面主题与调色板。' }, - projects: { label: '工作区', description: '管理 Runtime Host 连接,以及默认 Host 上的项目。' }, - models: { label: '模型', description: '模型连接、API key 与 OAuth 订阅管理。' }, - subagents: { label: '子 Agent', description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' }, - usage: { label: '使用统计', description: 'token、模型、工具使用走势与配额追踪。' }, - 'archived-tasks': { label: '已归档任务', description: '恢复或彻底删除已归档的任务。' }, - 'import-tasks': { label: '导入任务', description: '把本机其他 Agent 的对话记录转换成 Maka 任务。' }, - memory: { label: '记忆', description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' }, - 'daily-review': { label: '每日回顾', description: '每天分析本机任务,生成摘要、遗漏提醒和建议。' }, - 'bot-chat': { label: '远程接入', description: '通过 Telegram、飞书、微信等平台从其他设备与 Maka 对话。' }, - search: { label: '联网搜索', description: '联网搜索供应商(如 Tavily)凭据与隐私边界。' }, - data: { label: '数据', description: '本地工作区路径、备份与恢复。' }, - permissions: { label: '权限与能力', description: '系统权限授予状态与 Maka 能力运行时检查。' }, - health: { label: '健康', description: '运行时连接、模型探针与本地健康状态。' }, - about: { label: '关于', description: '版本、更新与隐私承诺。' }, - }, + general: { + label: '通用', + description: '显示名称与界面语言、隐私与通知、任务默认与网络代理。' + }, + appearance: { + label: '外观', + description: '界面主题与调色板。' + }, + projects: { + label: '工作区', + description: '管理 Runtime Host 连接,以及默认 Host 上的项目。' + }, + models: { + label: '模型', + description: '模型连接、API key 与 OAuth 订阅管理。' + }, + subagents: { + label: '子 Agent', + description: '配置主 Agent 可以自动选择的子 Agent、能力边界与模型。' + }, + usage: { + label: '使用统计', + description: 'token、模型、工具使用走势与配额追踪。' + }, + 'archived-tasks': { + label: '已归档任务', + description: '恢复或彻底删除已归档的任务。' + }, + 'import-tasks': { + label: '导入任务', + description: '把本机其他 Agent 的对话记录转换成 Maka 任务。' + }, + memory: { + label: '记忆', + description: 'Maka 记住的内容,以及本地 MEMORY.md 文件。' + }, + 'daily-review': { + label: '每日回顾', + description: '每天分析本机任务,生成摘要、遗漏提醒和建议。' + }, + 'bot-chat': { + label: '远程接入', + description: '通过 Telegram、飞书、微信等平台从其他设备与 Maka 对话。' + }, + search: { + label: '联网搜索', + description: '联网搜索供应商(如 Tavily)凭据与隐私边界。' + }, + data: { + label: '数据', + description: '本地工作区路径、备份与恢复。' + }, + permissions: { + label: '权限与能力', + description: '系统权限授予状态与 Maka 能力运行时检查。' + }, + health: { + label: '健康', + description: '运行时连接、模型探针与本地健康状态。' + }, + about: { + label: '关于', + description: '版本、更新与隐私承诺。' + } + } }, en: { groups: { preferences: 'Preferences', capabilities: 'Capabilities', activity: 'Activity', - system: 'System', + system: 'System' }, sections: { - general: { label: 'General', description: 'Display name and interface language, privacy and notifications, task defaults, and network proxy.' }, - appearance: { label: 'Appearance', description: 'Interface theme and color palette.' }, - projects: { label: 'Workspace', description: 'Manage Runtime Host connections and projects on the default Host.' }, - models: { label: 'Models', description: 'Model connections, API keys, and OAuth subscriptions.' }, - subagents: { label: 'Subagents', description: 'Configure the subagents, capability boundaries, and models the main agent may select.' }, - usage: { label: 'Usage', description: 'Token, model, tool usage trends, and quota tracking.' }, - 'archived-tasks': { label: 'Archived tasks', description: 'Restore or permanently delete archived tasks.' }, - 'import-tasks': { label: 'Import tasks', description: 'Convert conversations from another local agent into Maka tasks.' }, - memory: { label: 'Memory', description: 'What Maka remembers, and the local MEMORY.md file.' }, - 'daily-review': { label: 'Daily Review', description: 'Analyze local tasks for summaries, reminders, and suggestions.' }, - 'bot-chat': { label: 'Remote Access', description: 'Chat with Maka from other devices through Telegram, Feishu, or WeChat.' }, - search: { label: 'Web Search', description: 'Credentials and privacy boundaries for providers such as Tavily.' }, - data: { label: 'Data', description: 'Local workspace paths, backup, and restore.' }, - permissions: { label: 'Permissions & Capabilities', description: 'System grants and runtime checks for Maka capabilities.' }, - health: { label: 'Health', description: 'Runtime connections, model probes, and local health status.' }, - about: { label: 'About', description: 'Version, updates, and privacy commitments.' }, + general: { + label: 'General', + description: 'Display name and interface language, privacy and notifications, task defaults, and network proxy.' + }, + appearance: { + label: 'Appearance', + description: 'Interface theme and color palette.' + }, + projects: { + label: 'Workspace', + description: 'Manage Runtime Host connections and projects on the default Host.' + }, + models: { + label: 'Models', + description: 'Model connections, API keys, and OAuth subscriptions.' + }, + subagents: { + label: 'Subagents', + description: 'Configure the subagents, capability boundaries, and models the main agent may select.' + }, + usage: { + label: 'Usage', + description: 'Token, model, tool usage trends, and quota tracking.' + }, + 'archived-tasks': { + label: 'Archived tasks', + description: 'Restore or permanently delete archived tasks.' + }, + 'import-tasks': { + label: 'Import tasks', + description: 'Convert conversations from another local agent into Maka tasks.' + }, + memory: { + label: 'Memory', + description: 'What Maka remembers, and the local MEMORY.md file.' + }, + 'daily-review': { + label: 'Daily Review', + description: 'Analyze local tasks for summaries, reminders, and suggestions.' + }, + 'bot-chat': { + label: 'Remote Access', + description: 'Chat with Maka from other devices through Telegram, Feishu, or WeChat.' + }, + search: { + label: 'Web Search', + description: 'Credentials and privacy boundaries for providers such as Tavily.' + }, + data: { + label: 'Data', + description: 'Local workspace paths, backup, and restore.' + }, + permissions: { + label: 'Permissions & Capabilities', + description: 'System grants and runtime checks for Maka capabilities.' + }, + health: { + label: 'Health', + description: 'Runtime connections, model probes, and local health status.' + }, + about: { + label: 'About', + description: 'Version, updates, and privacy commitments.' + } + } + } +} satisfies Omit, 'ko'>; +const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { + ...SETTINGS_NAVIGATION_COPY_BY_LOCALE_BASE, + ko: { + groups: { + preferences: "환경설정", + capabilities: "기능", + activity: "활동", + system: "체계" }, - }, + sections: { + general: { + label: "일반적인", + description: "표시 이름 및 인터페이스 언어, 개인정보 보호 및 알림, 작업 기본값, 네트워크 프록시." + }, + appearance: { + label: "모습", + description: "인터페이스 테마 및 색상 팔레트." + }, + projects: { + label: "작업공간", + description: "기본 호스트에서 런타임 호스트 연결 및 프로젝트를 관리합니다." + }, + models: { + label: "모델", + description: "모델 연결, API 키 및 OAuth 구독." + }, + subagents: { + label: "하위 에이전트", + description: "기본 에이전트가 선택할 수 있는 하위 에이전트, 기능 경계 및 모델을 구성합니다." + }, + usage: { + label: "용법", + description: "토큰, 모델, 도구 사용 추세 및 할당량 추적." + }, + 'archived-tasks': { + label: "보관된 작업", + description: "보관된 작업을 복원하거나 영구적으로 삭제합니다." + }, + 'import-tasks': { + label: "작업 가져오기", + description: "다른 로컬 에이전트의 대화를 Maka 작업으로 변환하세요." + }, + memory: { + label: "메모리", + description: "마카가 기억하는 것과 로컬 MEMORY.md 파일." + }, + 'daily-review': { + label: "일일 검토", + description: "요약, 알림, 제안을 위해 로컬 작업을 분석합니다." + }, + 'bot-chat': { + label: "원격 액세스", + description: "Telegram, Feishu 또는 WeChat을 통해 다른 장치에서 Maka와 채팅하세요." + }, + search: { + label: "웹 검색", + description: "Tavily와 같은 제공업체에 대한 자격 증명 및 개인 정보 보호 경계." + }, + data: { + label: "데이터", + description: "로컬 작업공간 경로, 백업 및 복원." + }, + permissions: { + label: "권한 및 기능", + description: "Maka 기능에 대한 시스템 부여 및 런타임 검사입니다." + }, + health: { + label: "건강", + description: "런타임 연결, 모델 프로브 및 로컬 상태." + }, + about: { + label: "에 대한", + description: "버전, 업데이트 및 개인 정보 보호 약속." + } + } + } } satisfies UiCatalog; - export function getSettingsNavigationCopy(locale: UiLocale): SettingsNavigationCopy { return SETTINGS_NAVIGATION_COPY_BY_LOCALE[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts index d4f31f31eb..bca37f6f4d 100644 --- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts @@ -272,7 +272,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { zh: { personalization: { saveFailed: '保存失败', displayName: '显示名称', displayNameHelp: 'Maka 在聊天里会以这个名字称呼你。留空就用默认的“你”。', displayNamePlaceholder: '例如:JK', displayNameUnset: '未设置,Maka 会称呼你“你”', displayNameChange: '更改', displayNameSet: '设置', - interfaceLanguage: '界面语言', interfaceLanguageHelp: '选择 Maka 界面的显示语言。切换后立即生效,重启后保持。', localeOptions: [['auto', '跟随系统'], ['zh', '中文'], ['en', 'English']], + interfaceLanguage: '界面语言', interfaceLanguageHelp: '选择 Maka 界面的显示语言。切换后立即生效,重启后保持。', localeOptions: [['auto', '跟随系统'], ['zh', '中文'], ['en', 'English'], ['ko', '한국어']], assistantTone: '助手语气偏好', assistantToneHelp: '最多 500 字,只影响回答的语气和风格。权限确认与安全规则不受影响;改动会自动保存。', assistantTonePlaceholder: '例如:技术严谨、偏简洁、不要 emoji。', }, sections: { @@ -369,7 +369,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { }, en: { personalization: { - saveFailed: 'Could not save', displayName: 'Display name', displayNameHelp: 'Maka uses this name when addressing you. Leave it blank to use “you”.', displayNamePlaceholder: 'For example: JK', displayNameUnset: 'Not set — Maka will say “you”', displayNameChange: 'Change', displayNameSet: 'Set', interfaceLanguage: 'Interface language', interfaceLanguageHelp: 'Choose the language used by Maka. Changes apply immediately and persist after restart.', localeOptions: [['auto', 'Follow system'], ['zh', '中文'], ['en', 'English']], assistantTone: 'Assistant tone', assistantToneHelp: 'Up to 500 characters. This changes response style only; permission and safety rules still apply. Changes save automatically.', assistantTonePlaceholder: 'For example: technically rigorous, concise, and no emoji.', + saveFailed: 'Could not save', displayName: 'Display name', displayNameHelp: 'Maka uses this name when addressing you. Leave it blank to use “you”.', displayNamePlaceholder: 'For example: JK', displayNameUnset: 'Not set — Maka will say “you”', displayNameChange: 'Change', displayNameSet: 'Set', interfaceLanguage: 'Interface language', interfaceLanguageHelp: 'Choose the language used by Maka. Changes apply immediately and persist after restart.', localeOptions: [['auto', 'Follow system'], ['zh', '中文'], ['en', 'English'], ['ko', '한국어']], assistantTone: 'Assistant tone', assistantToneHelp: 'Up to 500 characters. This changes response style only; permission and safety rules still apply. Changes save automatically.', assistantTonePlaceholder: 'For example: technically rigorous, concise, and no emoji.', }, sections: { identity: 'Identity', identityHelp: 'How Maka addresses you, plus interface language and response tone.', @@ -428,6 +428,67 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { }, password: { copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', copying: 'Copying', copied: 'Copied', copy: 'Copy', hide: 'Hide', show: 'Show', value: 'credential value' }, }, + ko: { + personalization: { + saveFailed: '저장하지 못했습니다', displayName: '표시 이름', displayNameHelp: 'Maka가 사용자를 부를 때 사용할 이름입니다. 비워 두면 “사용자”라고 표시합니다.', displayNamePlaceholder: '예: 민준', displayNameUnset: '설정되지 않음 — Maka가 “사용자”라고 표시합니다', displayNameChange: '변경', displayNameSet: '설정', interfaceLanguage: '인터페이스 언어', interfaceLanguageHelp: 'Maka에서 사용할 언어를 선택하세요. 변경 사항은 즉시 적용되며 다시 시작해도 유지됩니다.', localeOptions: [['auto', '시스템 설정 따름'], ['zh', '中文'], ['en', 'English'], ['ko', '한국어']], assistantTone: '어시스턴트 말투', assistantToneHelp: '최대 500자입니다. 응답 스타일만 변경하며 권한 및 안전 규칙에는 영향을 주지 않습니다. 변경 사항은 자동 저장됩니다.', assistantTonePlaceholder: '예: 기술적으로 엄밀하고 간결하며 이모지 없이.', + }, + sections: { + identity: 'Identity', identityHelp: 'How Maka addresses you, plus interface language and response tone.', + privacy: 'Privacy and notifications', privacyHelp: 'What Maka may read and write locally, and when it notifies you.', + chatDefaults: 'Task defaults', chatDefaultsHelp: 'The model, permission mode, and thinking level a new task starts on.', + shell: 'Command environment', shellHelp: 'Choose the shell the Runtime Host uses for Bash tools and terminal commands.', + network: 'Network', networkHelp: 'The network path AI model requests take.', + theme: 'Theme', themeHelp: 'Follow the system appearance, or stay on light or dark.', + palette: 'Color palette', paletteHelp: 'Accent and canvas colors. Changes apply immediately and are saved locally.', + appIcon: 'App icon', appIconHelp: 'The Maka icon shown in the dock, taskbar, and app switcher. Changes apply immediately.', + fontSize: 'Font size', fontSizeHelp: 'Text size across the interface and terminal. Changes apply immediately and are saved locally.', + pets: 'Custom pets', petsHelp: 'Manage PetPacks you import yourself. Maka does not bundle or enable any pet by default.', + }, + appearance: { + saveFailed: 'Could not save appearance settings', theme: 'Theme', palette: 'Color palette', themeOptions: { light: { label: 'Light', help: 'Always use the light interface.' }, dark: { label: 'Dark', help: 'Always use the dark interface.' }, auto: { label: 'Follow system', help: 'Match the current system appearance.' } }, paletteLabels: { default: 'Default', onedark: 'One Dark', 'catppuccin-mocha': 'Catppuccin Mocha', 'tokyo-night': 'Tokyo Night', nord: 'Nord', coral: 'Coral', azure: 'Azure', forest: 'Forest', dusk: 'Dusk', sand: 'Sand', mono: 'Monochrome' }, paletteHelp: { default: 'Maka brand-blue accent', onedark: 'Classic dark editor theme', 'catppuccin-mocha': 'Soft purple dark theme', 'tokyo-night': 'Deep-blue editor theme', nord: 'Cool Nordic colors', coral: 'Warm pink and coral accent', azure: 'Clean, calm blue accent', forest: 'Deep moss and warm honey', dusk: 'Deep violet on a cool canvas', sand: 'Amber sand and warm ivory', mono: 'Pure grayscale without color distraction' }, paletteGroups: { editor: 'Editor themes', product: 'Product colors' }, appIconLabels: { default: 'Classic', mono: 'Monochrome', 'sky': 'Sky', 'cyan': 'Cyan', 'ice': 'Ice', 'pale-inverted': 'Inverted', 'ink': 'Ink', 'paper': 'Paper', 'graphite': 'Graphite', 'pencil-kraft': 'Pencil, kraft', 'pencil-sky': 'Pencil, sky', 'pencil-navy': 'Pencil, navy', 'alpine': 'Alpine', 'dusk': 'Dusk', 'night': 'Night', 'midnight': 'Midnight', 'carbon': 'Carbon', 'slate': 'Slate', 'obsidian': 'Obsidian', 'neon-cyan': 'Neon cyan', 'matrix': 'Phosphor', 'magenta': 'Magenta', 'amber-crt': 'Amber CRT', 'clay': 'Clay', 'sage': 'Sage', 'dust': 'Dust', 'fog': 'Fog', 'sunset': 'Sunset', 'amber': 'Amber', 'terracotta': 'Terracotta', 'ocean': 'Ocean', 'moss': 'Moss', 'desert': 'Desert', 'glacier': 'Glacier', 'gold': 'Gold', 'chrome': 'Chrome', 'mono-black': 'Mono black', 'mono-white': 'Mono white', 'hazard': 'Hazard', 'forest': 'Forest' }, appIconHelp: { default: 'The default Maka mark', mono: 'Grayscale, for a quieter dock', 'sky': 'The geometric M mark in brand blue', 'cyan': 'Blue leaning to cyan', 'ice': 'A pale-to-deep blue gradient', 'pale-inverted': 'A deep blue mark on a pale field', 'ink': 'White on black, the highest contrast', 'paper': 'Black on white', 'graphite': 'Black on white with a grey tip', 'pencil-kraft': 'The pencil reading, on kraft paper', 'pencil-sky': 'The pencil reading, on sky blue', 'pencil-navy': 'The pencil reading, on deep navy', 'alpine': 'A snow-capped peak under clear sky', 'dusk': 'A snow-capped peak at dusk', 'night': 'A snow-capped peak at night', 'midnight': 'A bright mark on deep navy; keeps its edge on a dark dock', 'carbon': 'True black, so an OLED panel shows nothing but the mark', 'slate': 'Pale grey on cool slate', 'obsidian': 'Lilac on a violet-black gradient', 'neon-cyan': 'Electric cyan on near-black', 'matrix': 'The green of a phosphor terminal', 'magenta': 'Hot pink on deep violet', 'amber-crt': 'The amber of an early terminal', 'clay': 'Muted terracotta', 'sage': 'Muted grey-green', 'dust': 'Muted dusty rose', 'fog': 'Muted blue-grey', 'sunset': 'An orange-to-pink diagonal', 'amber': 'A dark mark on amber', 'terracotta': 'A brick-red gradient', 'ocean': 'A deep teal gradient', 'moss': 'A deep moss gradient', 'desert': 'A dark mark on desert sand', 'glacier': 'A pale glacial blue', 'gold': 'The mark itself carries a gold gradient', 'chrome': 'The mark itself carries a silver gradient', 'mono-black': 'Black on pure white; prints in one colour', 'mono-white': 'White on pure black', 'hazard': 'Yellow on black, the highest contrast in the set', 'forest': 'A snow-capped peak in green' }, appIconGroups: { mascot: 'Mascot', blue: 'Blues', contrast: 'Black & white', pencil: 'Pencil', mountain: 'Mountain', dark: 'Dark', neon: 'Neon', muted: 'Muted', warm: 'Warm', nature: 'Nature', metal: 'Metal', highContrast: 'High contrast', custom: 'Imported' }, appIconSplitLabel: 'Use a different icon in dark mode', appIconSplitHelp: 'When off, one icon is used in both appearances.', appIconTargets: { light: 'Light', dark: 'Dark' }, appIconCustom: 'Imported icon', appIconCustomHelp: 'An image you imported', appIconImport: 'Import icon…', appIconImporting: 'Importing…', appIconImportHelp: 'A square PNG works best. Leave about 10% transparent margin so it sits the same size as other apps in the dock.', appIconRemove: 'Remove', appIconImportError: 'Could not import the icon', appIconRemoveFailed: 'Could not remove the icon', appIconSelectFailed: 'Could not switch the icon', appIconImportFailed: { too_large: 'That file is too large; pick a smaller image', too_many_pixels: 'That image is too large; 4096×4096 is the maximum', unsupported_format: 'Only PNG and JPEG are supported', unreadable: 'No image could be read from that file', too_small: 'That image is too small; 128×128 is the minimum', write_failed: 'Could not store the imported icon' }, appIconUnavailable: 'Could not load the app icons', fontSize: { uiLabel: 'UI font size', uiHelp: 'Base font size used across the interface', terminalLabel: 'Terminal font size', terminalHelp: 'Font size used for terminal output and code' }, + }, + pets: { + import: 'Import PetPack', importing: 'Importing…', loading: 'Loading custom pets…', + status: 'Desktop pet', activePet: (name) => `Currently using: ${name}`, disabled: 'Off', disable: 'Turn off pet', disabling: 'Turning off…', + empty: 'No pets imported yet', emptyHelp: 'Choose a local folder containing pet.json and a sprite sheet.', + selected: 'In use', select: 'Use', selecting: 'Switching…', remove: 'Remove', removing: 'Removing…', + removeTitle: (name) => `Remove “${name}”?`, removeDescription: 'This removes Maka’s local copy of the pet pack and cannot be undone. The original folder is not affected.', confirmRemove: 'Remove', cancel: 'Cancel', + loadFailed: 'Could not load custom pets', importFailed: 'Could not import pet', selectFailed: 'Could not switch pet', removeFailed: 'Could not remove pet', + importErrors: { invalid_directory: 'The selected folder is invalid.', invalid_manifest: 'pet.json does not match the maka.pet/v1 format.', invalid_asset: 'The sprite sheet is missing, invalid, or outside the supported limits.', already_installed: 'A pet with the same ID is already installed.', read_failed: 'The selected folder could not be read.' }, + selectErrors: { invalid_id: 'The pet ID is invalid.', not_found: 'That pet is no longer in the local library.', read_failed: 'The pet library could not be read.', write_failed: 'The pet selection could not be saved.' }, + removeErrors: { invalid_id: 'The pet ID is invalid.', remove_failed: 'The local pet pack could not be removed.' }, + }, + general: { + incognito: 'Incognito mode', incognitoHelp: 'Pause local memory, web search, and scheduled task triggers.', enableIncognito: 'Enable incognito mode', incognitoFailed: 'Could not change incognito mode', notifications: 'Send a system notification when finished', notificationsHelp: 'Notify when a response finishes or fails while the window is in the background.', notificationsFailed: 'Could not change notification settings', workspaceInstructions: 'Follow project instructions', workspaceInstructionsHelp: 'Automatically read existing AGENTS.md, CLAUDE.md, or GEMINI.md files in each project. Manage the files in their respective projects.', workspaceInstructionsFailed: 'Could not change project instruction settings', workHub: 'Enable WorkHub', workHubHelp: 'WorkHub is not available yet. This toggle is for development testing and does not enable a usable feature.', workHubFailed: 'Could not change WorkHub setting', updateFailed: 'The setting was not applied. Try again later.', defaultModel: 'Default model', defaultModelHelp: 'Model used by new tasks.', notSet: 'Not set', saveDefaultModelFailed: 'Could not save the default model', defaultPermission: 'Default permission mode', defaultPermissionHelp: 'Initial permission mode for new tasks; it can be changed at any time.', saveDefaultPermissionFailed: 'Could not save the default permission mode', defaultThinking: 'Default thinking level', defaultThinkingHelp: 'Thinking level for new tasks; models that do not offer the chosen level use their own default.', followModelDefault: 'Follow model default', saveDefaultThinkingFailed: 'Could not save the default thinking level', proxy: 'Proxy server', proxyHelp: 'Configure a network proxy for AI model requests', enableProxy: 'Enable proxy server', saveNetworkFailed: 'Could not save network settings', proxyProtocol: 'Proxy protocol', serverAddress: 'Server address', port: 'Port', proxyAuth: 'Proxy authentication', proxyAuthHelp: 'Enable this when a username and password are required.', enableProxyAuth: 'Enable proxy authentication', username: 'Username', password: 'Password', bypassList: 'Proxy bypass list', bypassHelp: 'These domains connect directly. Separate multiple domains with commas.', autoBypass: (count) => `${count} ${count === 1 ? 'domain was' : 'domains were'} added automatically. The proxy applies to AI model requests only.`, testing: 'Testing…', testCurrent: 'Test current configuration', proxyReachable: 'Proxy is reachable', proxyTestFailed: 'Proxy test failed', proxyTestError: 'Could not test proxy', + shellPreference: 'Bash tool shell', shellPreferenceHelp: 'Automatic keeps the PowerShell-first Windows default. Git Bash is an explicit override for the current Runtime Host.', shellAuto: 'Automatic (recommended)', shellGitBash: 'Git Bash', shellExecutable: 'Git Bash executable', shellExecutableHelp: 'Enter the absolute path to bash.exe on the Windows machine running the Runtime Host. The legacy System32 WSL Bash shim is also recognized; Maka verifies GNU Bash before saving.', saveShell: 'Save shell setting', savingShell: 'Saving…', shellSaved: 'Saved', saveShellFailed: 'Could not save shell setting', shellExecutableRejected: 'The current Runtime Host could not run that path as GNU Bash. Check that the Host runs Windows, the path exists, and the file is named bash.exe.', + passwordSavedPlaceholder: 'Password saved; enter a new password to replace it', + }, + about: { + loadFailed: 'Could not load About information', loading: 'Loading About', unavailable: 'About information is unavailable', copied: 'Diagnostics copied', pasteHint: 'Review the content, then paste it into an issue report', copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', devBuild: 'Local development build', nightlyBuild: 'Nightly', buildLabel: 'Build', + channelSummaries: { + dev: 'A local development build. It does not check for updates.', + nightly: 'A daily prerelease build. It updates itself to the latest nightly and replaces a release install.', + release: 'The official release build. It receives stable updates automatically.', + }, + supportTitle: 'Support', + copying: 'Copying…', copyDiagnostics: 'Copy diagnostics', copyAction: 'Copy', copyHelp: 'Copy version, platform, a home-redacted workspace path, and recent redacted logs. The report is written only to the clipboard and is never uploaded automatically.', + reportIssueLabel: 'Report an issue', reportIssueHelp: 'Open a GitHub issue with your diagnostics attached — replies come faster.', reportIssueOpen: 'Open', + keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', + privacyLabel: 'Privacy commitments', privacyTitle: 'Local first · Private by default', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay on this machine; model keys live in a local credential file and subscription tokens use secure system storage.', 'Maka sends no usage telemetry and contacts a model provider only when you enable it.', 'High-risk tool operations require explicit permission in the task; messages, tool calls, permission decisions, and mode changes are retained locally for each task.'], + checkForUpdates: 'Check for updates', + checkingForUpdates: 'Checking…', + updateIdle: 'No update check has run yet.', + updateNotAvailable: 'You are on the latest version.', + updateAvailable: (version) => `Version v${version} is available and will download shortly…`, + updateDownloading: (version, percent) => `Downloading v${version} (${percent}%)…`, + updateVerifying: (version) => `Verifying the release provenance for v${version}…`, + updateDownloaded: (version) => `v${version} is ready. Restart from the sidebar to install.`, + updateInstalling: (version) => `Installing v${version}…`, + updateCheckFailed: 'Could not check for updates', + updateCheckFailedDetail: (message) => message, + }, + password: { copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', copying: 'Copying', copied: 'Copied', copy: 'Copy', hide: 'Hide', show: 'Show', value: 'credential value' }, + } } satisfies UiCatalog; export function getSettingsPreferencesCopy(locale: UiLocale): SettingsPreferencesCopy { diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts index 3666a18ddd..82eabcc8ca 100644 --- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts @@ -18,7 +18,6 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - export type SettingsProjectsCopy = { runtimeHost: { title: string; @@ -210,16 +209,8 @@ export type SettingsProjectsCopy = { updatePolicyActiveTasks: string; updatePolicyNotNewer(version: string): string; updatePolicyManualAction(version: string): string; - updatePolicyManualReason: Record< - | 'current_compatibility_unknown' - | 'target_compatibility_unknown' - | 'compatibility_mismatch', - string - >; - updatePhase: Record< - 'preparing_cli' | import('@maka/runtime-host/operator').RuntimeHostServiceUpdatePhase, - string - >; + updatePolicyManualReason: Record<'current_compatibility_unknown' | 'target_compatibility_unknown' | 'compatibility_mismatch', string>; + updatePhase: Record<'preparing_cli' | import('@maka/runtime-host/operator').RuntimeHostServiceUpdatePhase, string>; updateBlockedTitle: string; updateBlockedBody: string; updateInterrupt: string; @@ -308,8 +299,7 @@ export type SettingsProjectsCopy = { */ moreActions(projectName: string): string; }; - -const SETTINGS_PROJECTS_COPY_BY_LOCALE = { +const SETTINGS_PROJECTS_COPY_BY_LOCALE_BASE = { zh: { runtimeHost: { title: 'Runtime Host', @@ -384,7 +374,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { installing_service: '正在启动 Runtime Host…', pairing_client: '正在配对这台设备…', verifying_connection: '正在验证凭据…', - connecting_host: '正在建立安全连接…', + connecting_host: '正在建立安全连接…' }, add: '添加远程 Host', cancel: '取消', @@ -431,7 +421,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { stopped: '已停止', starting: '正在启动', running: '运行中', - failed: '启动失败', + failed: '启动失败' }, directPeer: 'Direct peer(实验性)', directPeerDescription: '创建独立的实验性 Direct profile。可自动发现或手动指定协调节点来辅助打洞;受限 NAT 或被阻止的 UDP 仍可能使其不可达,且不会回退到中继传输。保留 SSH profile 用于手动恢复。', @@ -440,7 +430,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { not_configured: '未配置', disabled: '已停用', enabled: '已启用', - unavailable: '不可用', + unavailable: '不可用' }, directPeerUnavailable: '无法读取 Direct peer 状态', directPeerUpgradeRequired: '请先更新远程 Runtime Host,再管理 Direct peer。', @@ -456,18 +446,14 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { directPeerStunPolicyOptions: { default: '公共 STUN(推荐)', disabled: '不使用公共 STUN', - custom: '自定义 STUN', + custom: '自定义 STUN' }, directPeerStunUrls: 'STUN 地址', - directPeerStunDefaultHelp: - '使用 Cloudflare 公共 STUN 尽力发现公网映射。它不转发 Maka 流量,但提供方可观察源 IP 和请求时间;Maka 不保证其可用性。', - directPeerStunDisabledHelp: - '仅尝试本地地址和其他已知直连路径;跨 NAT 的直连成功率可能降低。', - directPeerStunCustomHelp: - '使用逗号分隔的 stun: 地址。STUN 只发现网络地址,不承载 Session 内容。', + directPeerStunDefaultHelp: '使用 Cloudflare 公共 STUN 尽力发现公网映射。它不转发 Maka 流量,但提供方可观察源 IP 和请求时间;Maka 不保证其可用性。', + directPeerStunDisabledHelp: '仅尝试本地地址和其他已知直连路径;跨 NAT 的直连成功率可能降低。', + directPeerStunCustomHelp: '使用逗号分隔的 stun: 地址。STUN 只发现网络地址,不承载 Session 内容。', directPeerAutomaticRelayDiscovery: '自动发现协调节点', - directPeerAutomaticRelayDiscoveryHelp: - '协调节点使用 Circuit Relay v2 协议,仅帮助建立端到端直连,不承载应用流量。Maka 会通过公共 IPFS 网络尽力发现可用节点;手动设置的节点优先。', + directPeerAutomaticRelayDiscoveryHelp: '协调节点使用 Circuit Relay v2 协议,仅帮助建立端到端直连,不承载应用流量。Maka 会通过公共 IPFS 网络尽力发现可用节点;手动设置的节点优先。', directPeerEnable: '启用并添加', directPeerDisable: '停用', directPeerAddProfile: '添加到 Desktop', @@ -510,7 +496,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { manual: '手动更新', fixed: '固定版本', latest: 'Latest 稳定频道', - next: 'Next 预览频道', + next: 'Next 预览频道' }, updatePolicyFixedVersion: '版本', updatePolicySave: '保存策略', @@ -530,14 +516,14 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { updatePolicyManualReason: { current_compatibility_unknown: '无法确认当前版本的存储兼容性', target_compatibility_unknown: '无法确认目标版本的存储兼容性', - compatibility_mismatch: '目标版本需要手动处理存储兼容性', + compatibility_mismatch: '目标版本需要手动处理存储兼容性' }, updatePhase: { preparing_cli: '正在准备本地 CLI…', checking: '正在检查版本…', staging: '正在准备新版本…', retiring: '正在安全停止当前 Runtime Host…', - replacing: '正在启动并验证新版本…', + replacing: '正在启动并验证新版本…' }, updateBlockedTitle: 'Runtime Host 可能仍在执行任务', updateBlockedBody: '无法确认当前 Host 可以安全停止。继续更新会中断当前执行,但会保留可恢复的任务状态和无法确认的外部效果。', @@ -560,7 +546,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { currentDesktop: '当前 Desktop', accessKind: { owner: '客户端访问', - capabilityProvider: 'Capability Provider', + capabilityProvider: 'Capability Provider' }, accessPending: '等待确认', accessCreated: (date: string) => `创建于 ${date}`, @@ -590,7 +576,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { discardPairingConfirmTitle: '放弃这次配对?', discardPairingConfirmBody: '将删除未完成的连接并清理本机保存的临时凭据。之后仍可使用新的邀请码重新加入。', discardPairingFailed: '无法放弃配对', - moreActions: (name: string) => `更多操作:${name}`, + moreActions: (name: string) => `更多操作:${name}` }, section: '工作区', // Says all three layers of the rule in one sentence, because a help line @@ -624,7 +610,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { defaultUnavailable: '原来的默认项目已不可用,新任务暂时沿用上次使用的项目。', emptyTitle: '还没有项目', emptyBody: '添加一个项目目录后,新任务就能默认从它打开,侧边栏也会按项目归类任务。', - moreActions: (projectName: string) => `更多操作:${projectName}`, + moreActions: (projectName: string) => `更多操作:${projectName}` }, en: { runtimeHost: { @@ -700,7 +686,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { installing_service: 'Starting Runtime Host…', pairing_client: 'Pairing this device…', verifying_connection: 'Verifying access…', - connecting_host: 'Establishing the secure connection…', + connecting_host: 'Establishing the secure connection…' }, add: 'Add remote Host', cancel: 'Cancel', @@ -747,7 +733,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { stopped: 'Stopped', starting: 'Starting', running: 'Running', - failed: 'Failed', + failed: 'Failed' }, directPeer: 'Direct peer (experimental)', directPeerDescription: 'Create an independent experimental Direct profile. Discover coordination peers automatically or provide them manually to assist hole punching; restrictive NAT or blocked UDP may still make it unreachable, and traffic does not fall back to a relay. Keep the SSH profile for manual recovery.', @@ -756,7 +742,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { not_configured: 'Not configured', disabled: 'Disabled', enabled: 'Enabled', - unavailable: 'Unavailable', + unavailable: 'Unavailable' }, directPeerUnavailable: 'Direct peer status is unavailable', directPeerUpgradeRequired: 'Update the remote Runtime Host before managing Direct peer.', @@ -772,18 +758,14 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { directPeerStunPolicyOptions: { default: 'Public STUN (recommended)', disabled: 'No public STUN', - custom: 'Custom STUN', + custom: 'Custom STUN' }, directPeerStunUrls: 'STUN addresses', - directPeerStunDefaultHelp: - '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.', - directPeerStunDisabledHelp: - 'Only local addresses and other known direct paths are attempted; direct connectivity across NAT may be reduced.', - directPeerStunCustomHelp: - 'Enter comma-separated stun: addresses. STUN discovers network addresses and never carries Session content.', + directPeerStunDefaultHelp: '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.', + directPeerStunDisabledHelp: 'Only local addresses and other known direct paths are attempted; direct connectivity across NAT may be reduced.', + directPeerStunCustomHelp: 'Enter comma-separated stun: addresses. STUN discovers network addresses and never carries Session content.', directPeerAutomaticRelayDiscovery: 'Discover coordination peers automatically', - directPeerAutomaticRelayDiscoveryHelp: - 'Coordination peers use Circuit Relay v2 only to establish an end-to-end direct connection; they never carry application traffic. Maka discovers candidates through the public IPFS network on a best-effort basis, while manually configured peers remain preferred.', + directPeerAutomaticRelayDiscoveryHelp: 'Coordination peers use Circuit Relay v2 only to establish an end-to-end direct connection; they never carry application traffic. Maka discovers candidates through the public IPFS network on a best-effort basis, while manually configured peers remain preferred.', directPeerEnable: 'Enable and add', directPeerDisable: 'Disable', directPeerAddProfile: 'Add to Desktop', @@ -826,22 +808,19 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { manual: 'Manual updates', fixed: 'Fixed version', latest: 'Latest stable channel', - next: 'Next preview channel', + next: 'Next preview channel' }, updatePolicyFixedVersion: 'Version', updatePolicySave: 'Save policy', updatePolicyCheckNow: 'Check now', updatePolicyUnavailable: 'Automatic update policy is unavailable', updateSchedulerUnavailable: 'Automatic updates are not available on this Runtime Host', - updateSchedulerUnavailableBody: - 'Update or repair the service before choosing a fixed version or release channel', + updateSchedulerUnavailableBody: 'Update or repair the service before choosing a fixed version or release channel', updateSchedulerUnsupported: 'Unsupported', updateSchedulerInactive: 'Inactive', - updateSchedulerInactiveBody: - 'The update scheduler is not running. Start or repair the service before enabling automatic updates', + updateSchedulerInactiveBody: 'The update scheduler is not running. Start or repair the service before enabling automatic updates', updateSchedulerNeedsRepair: 'Needs repair', - updateSchedulerNeedsRepairBody: - 'The update scheduler is not running. Repair the service before enabling automatic updates', + updateSchedulerNeedsRepairBody: 'The update scheduler is not running. Repair the service before enabling automatic updates', updatePolicyDisabled: 'Automatic updates are off', updatePolicyActiveTasks: 'Runtime Host owns active work, so this update was deferred', updatePolicyNotNewer: (version: string) => `Maka ${version} is not newer than this Host`, @@ -849,14 +828,14 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { updatePolicyManualReason: { current_compatibility_unknown: 'The installed version has unknown storage compatibility', target_compatibility_unknown: 'The target version has unknown storage compatibility', - compatibility_mismatch: 'The target requires a manual storage compatibility decision', + compatibility_mismatch: 'The target requires a manual storage compatibility decision' }, updatePhase: { preparing_cli: 'Preparing the local CLI…', checking: 'Checking versions…', staging: 'Staging the new version…', retiring: 'Safely stopping the current Runtime Host…', - replacing: 'Starting and verifying the new version…', + replacing: 'Starting and verifying the new version…' }, updateBlockedTitle: 'Runtime Host may still own active work', updateBlockedBody: 'Desktop could not prove that the current Host can stop safely. Continuing will interrupt current execution while preserving recoverable task state and unresolved external effects.', @@ -879,7 +858,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { currentDesktop: 'This Desktop', accessKind: { owner: 'Client access', - capabilityProvider: 'Capability provider', + capabilityProvider: 'Capability provider' }, accessPending: 'Pending confirmation', accessCreated: (date: string) => `Created ${date}`, @@ -909,11 +888,10 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { discardPairingConfirmTitle: 'Discard this pairing?', discardPairingConfirmBody: 'This removes the unfinished connection and its locally saved temporary credential. You can join again with a new invitation.', discardPairingFailed: 'Could not discard pairing', - moreActions: (name: string) => `More actions for ${name}`, + moreActions: (name: string) => `More actions for ${name}` }, section: 'Workspace', - sectionHelp: - 'New tasks open in the default project; without one, they reuse the project you last used. You can switch any task to a different project next to the input box.', + sectionHelp: 'New tasks open in the default project; without one, they reuse the project you last used. You can switch any task to a different project next to the input box.', addProject: 'Add project', defaultBadge: 'Default', setDefault: 'Set as default', @@ -930,21 +908,326 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { clearDefault: 'Clear default', remove: 'Remove from Maka', removeConfirmTitle: 'Remove this project from Maka?', - removeConfirmBody: - 'This only removes it from Maka’s project list; the files on disk are untouched. Tasks under this project move to “Ungrouped” and are not deleted.', + removeConfirmBody: 'This only removes it from Maka’s project list; the files on disk are untouched. Tasks under this project move to “Ungrouped” and are not deleted.', removeConfirm: 'Remove', removeCancel: 'Cancel', actionFailed: 'Action failed', unavailable: 'Folder unavailable', - defaultUnavailable: - 'The default project is no longer available, so new tasks reuse the project you last used.', + defaultUnavailable: 'The default project is no longer available, so new tasks reuse the project you last used.', emptyTitle: 'No projects yet', - emptyBody: - 'Add a project folder and new tasks can start in it, with the sidebar grouping tasks by project.', - moreActions: (projectName: string) => `More actions for ${projectName}`, - }, + emptyBody: 'Add a project folder and new tasks can start in it, with the sidebar grouping tasks by project.', + moreActions: (projectName: string) => `More actions for ${projectName}` + } +} satisfies Omit, 'ko'>; +const SETTINGS_PROJECTS_COPY_BY_LOCALE = { + ...SETTINGS_PROJECTS_COPY_BY_LOCALE_BASE, + ko: { + runtimeHost: { + title: "런타임 호스트", + description: "로컬 및 기타 활성화된 호스트는 함께 연결되어 있습니다. 각 작업은 해당 호스트의 소유로 유지됩니다.", + selected: "기본 호스트", + selectedHelp: "새로운 작업과 범위가 지정되지 않은 설정은 기본 호스트를 사용합니다.", + remoteTitle: "다른 호스트", + remoteDescription: "연결 코드 또는 안내 설정을 통해 런타임 호스트를 추가하고 관리하세요.", + addComputer: "컴퓨터 추가", + useConnectionCode: "연결 코드 사용", + useConnectionCodeDescription: "다른 컴퓨터에서 생성된 일회용 코드 붙여넣기", + addSshComputer: "SSH를 통해 설정", + addSshComputerDescription: "SSH로 접속할 수 있는 컴퓨터에 호스트를 설치하고 연결하세요.", + addWslEnvironment: "WSL 환경 추가", + addWslEnvironmentDescription: "이 Windows 컴퓨터의 WSL 내부에 호스트 설치 및 연결", + configureManually: "수동으로 구성", + configureManuallyDescription: "기존 호스트에 대한 TLS, SSH 또는 직접 피어 세부 정보를 입력하세요.", + thisComputerRemoteAccess: "원격 액세스", + thisComputerRemoteAccessHelp: "자동 공개 조정 검색을 통해 실험적인 엔드투엔드 직접 연결을 통해 이 호스트에 도달", + remoteAccessOn: "~에", + remoteAccessOff: "끄다", + enableRemoteAccess: "할 수 있게 하다", + disableRemoteAccess: "연결 끄기", + disableRemoteAccessConfirm: "원격 연결을 끄시겠습니까?", + disableRemoteAccessDescription: "이렇게 하면 직접 피어 연결만 중지됩니다. 부여된 공유 액세스 권한은 유지됩니다.", + revokeSharedAccess: "공유 액세스 취소", + revokeSharedAccessConfirm: "공유 액세스를 취소하시겠습니까?", + revokeSharedAccessDescription: "연결된 데스크톱의 연결이 끊어지고, 사용하지 않은 연결 코드의 작동이 중지됩니다.", + revokeSharedAccessDone: "공유 액세스가 취소되었습니다.", + createConnectionCode: "새로운 연결 코드", + connectionCodeTitle: "이 컴퓨터에 연결", + connectionCodeDescription: "15분 후에 만료되며 한 번만 사용할 수 있습니다. 다른 데스크탑은 소유자 액세스 권한을 받습니다. 직접 피어에는 대체가 없습니다.", + importConnectionCodeTitle: "연결 코드 사용", + importConnectionCodeDescription: "연결하면 이 데스크톱 소유자에게 다른 호스트에 대한 액세스 권한이 부여됩니다. 직접 피어에는 대체가 없습니다.", + connectionCodeHelpLabel: "연결 코드를 얻는 방법", + connectionCodeHelp: "대상 컴퓨터에서 Maka 설정 → 작업 공간 → 원격 액세스를 열거나 SSH 관리 호스트를 열고 새 연결 코드를 선택합니다. 대상 컴퓨터에서 maka 런타임 호스트 액세스 연결 코드를 실행할 수도 있습니다. 코드는 15분 후에 만료되며 한 번만 작동합니다.", + connectionCode: "연결 코드", + copyConnectionCode: "연결 코드 복사", + pasteConnectionCode: "반죽", + connectionCodeCopied: "연결 코드가 복사되었습니다.", + connectionCodeInvalid: "연결 코드가 잘못되었습니다.", + connectionCodeUnavailable: "연결 코드가 만료되었거나 이미 사용되었습니다. 다른 컴퓨터에서 새 코드를 만듭니다.", + connectionCodeHostUnreachable: "직접 피어 연결을 설정할 수 없습니다. 두 컴퓨터가 모두 온라인 상태이고 UDP가 허용되는지 확인하세요.", + connectionCodeHostMismatch: "코드가 연결된 호스트와 일치하지 않거나 호스트 버전이 호환되지 않습니다.", + connectionCodeUnknownError: "연결 결과를 알 수 없습니다. 다시 시도하기 전에 원격 호스트 목록을 확인하세요.", + connectWithCode: "연결하다", + remoteAccessActiveTasks: "이 컴퓨터에는 아직 실행 중인 작업이 있습니다.", + remoteAccessActiveTasksDescription: "원격 액세스를 활성화하면 로컬 호스트가 시스템 서비스로 전달됩니다. 현재 작업을 중단하고 계속하시겠습니까?", + uninstallActiveTasksDescription: "백그라운드 서비스를 제거하면 현재 작업이 중지됩니다. 중단하고 계속하시겠습니까?", + interruptAndEnable: "중단 및 활성화", + interruptAndUninstall: "중단 및 제거", + remoteAccessFailed: "원격 액세스 실패", + setupTitle: "런타임 호스트 추가", + setupSshDescription: "SSH를 통해 사용 가능한 컴퓨터에 런타임 호스트 설치 및 연결", + setupWslDescription: "로컬 WSL 환경에 런타임 호스트 설치 및 연결", + setupName: "표시 이름(선택사항)", + wslDistribution: "WSL 배포", + setupSshPort: "SSH 포트(선택 사항)", + setupDirectoryRootsDescription: "원격 홈 디렉터리를 사용하려면 비워 두세요. 디렉터리가 추가되면 해당 위치만 찾아 프로젝트를 추가할 수 있습니다.", + setupConnect: "연결하다", + setupCancel: "취소", + setupRetry: "다시 해 보다", + setupDone: "완료", + setupChooseProject: "프로젝트 선택", + setupComplete: "런타임 호스트가 연결됨", + setupPhase: { + preparing_cli: "로컬 CLI 준비 중…", + connecting_ssh: "SSH를 통해 연결하는 중…", + connecting_wsl: "WSL 환경에 연결하는 중…", + checking_environment: "원격 환경을 확인하는 중…", + installing_package: "마카 설치 중…", + installing_service: "런타임 호스트 시작 중...", + pairing_client: "이 기기를 페어링하는 중…", + verifying_connection: "액세스 확인 중…", + connecting_host: "보안 연결 설정 중…" + }, + add: "원격 호스트 추가", + cancel: "취소", + name: "표시 이름", + nameHelp: "이 장치에서 이 호스트를 식별하는 데에만 사용됩니다.", + transport: "연결 방법", + transportHelp: "TLS를 선호하거나 SSH 터널을 사용하여 개인 컴퓨터의 루프백 전용 호스트에 연결", + tls: 'TLS', + ssh: "SSH 터널", + plaintext: "일반 웹소켓", + url: 'WSS URL', + urlHelp: "원격 런타임 호스트의 wss:// 주소", + plaintextUrl: 'WS URL', + plaintextUrlHelp: "원격 런타임 호스트의 ws:// 주소", + sshDestination: "SSH 대상", + sshDestinationHelp: "OpenSSH user@host 대상 또는 SSH 구성 별칭", + sshPort: "SSH 포트", + sshPortHelp: "선택 과목; OpenSSH 기본 또는 SSH 구성을 사용하려면 비워 두세요.", + remotePort: "원격 호스트 포트", + remotePortHelp: "런타임 호스트가 127.0.0.1을 원격으로 수신하는 WebSocket 포트", + websocketPath: "웹소켓 경로", + websocketPathHelp: "일반적으로 /runtime-host", + plaintextAcknowledgement: "나는 일반 텍스트의 위험을 이해합니다", + plaintextAcknowledgementHelp: "액세스 자격 증명 및 데이터는 네트워크의 다른 사람에 의해 가로채질 수 있습니다.", + plaintextWarning: "신뢰할 수 있고 격리된 네트워크에서만 사용하세요. 공개 연결은 TLS 또는 SSH 터널을 사용해야 합니다.", + sshTerminalTitle: "원격 런타임 호스트에 연결", + sshTerminalDescription: "OpenSSH 프롬프트에 따라 호스트를 신뢰하거나 비밀번호를 입력하세요. 기존 SSH 키에는 일반적으로 입력이 필요하지 않습니다.", + sshTerminalClosed: "SSH 연결이 종료되었습니다", + sshTerminalClose: "닫다", + rootId: "상태 루트 ID", + rootIdHelp: "예상 호스트를 확인하기 위해 원격 서비스 준비 출력에서 ​​복사됨", + credential: "액세스 자격 증명", + credentialHelp: "데스크탑-클라이언트 사전 설정을 사용하여 원격 시스템에서 발행하십시오.", + saveAndEnable: "저장 및 활성화", + defaultBadge: "기본", + experimentalBadge: "실험적", + defaultDisableHelp: "이 호스트를 비활성화하기 전에 다른 기본 호스트를 선택하세요", + unavailable: "없는", + manage: "관리하다", + managementTitle: (name: string) => `${name} 관리`, + serviceStatus: "서비스 상태", + serviceState: { + not_installed: "설치되지 않음", + stopped: "중지됨", + starting: "시작", + running: "달리기", + failed: "실패한" + }, + directPeer: "직접 피어(실험적)", + directPeerDescription: "독립적인 실험적 직접 프로필을 만듭니다. 조정 피어를 자동으로 검색하거나 수동으로 제공하여 홀 펀칭을 지원합니다. 제한적인 NAT 또는 차단된 UDP로 인해 여전히 연결이 불가능할 수 있으며 트래픽은 릴레이로 대체되지 않습니다. 수동 복구를 위해 SSH 프로필을 유지하세요.", + directPeerState: { + unsupported: "업데이트 필요", + not_configured: "구성되지 않음", + disabled: "장애가 있는", + enabled: "활성화됨", + unavailable: "없는" + }, + directPeerUnavailable: "다이렉트 피어 상태를 사용할 수 없습니다.", + directPeerUpgradeRequired: "다이렉트 피어를 관리하기 전에 원격 런타임 호스트를 업데이트하세요.", + directPeerClientUnavailable: "이 데스크탑 빌드에는 다이렉트 피어 지원이 포함되어 있지 않습니다.", + directPeerDisableProfileFirst: "먼저 런타임 호스트 목록에서 직접 피어를 비활성화합니다.", + directPeerId: "피어 ID", + directPeerRoutes: "노선", + directPeerCoordinationRelays: "연결 조정 피어(선택 사항)", + directPeerCoordinationRelaysPlaceholder: "여러 주소를 쉼표로 구분하세요.", + directPeerAdvancedCoordination: "조정 피어를 수동으로 설정", + directPeerAdvancedNatTraversal: "NAT 통과(고급)", + directPeerStunPolicy: "공개 주소 검색", + directPeerStunPolicyOptions: { + default: "공개 STUN(권장)", + disabled: "공개 STUN 없음", + custom: "맞춤형 기절" + }, + directPeerStunUrls: "STUN 주소", + directPeerStunDefaultHelp: "Cloudflare 공개 STUN을 최선의 방식으로 사용하여 공개 매핑을 검색합니다. Maka 트래픽을 전달하지 않지만 공급자는 소스 IP와 요청 타이밍을 관찰할 수 있습니다. Maka는 가용성을 보장하지 않습니다.", + directPeerStunDisabledHelp: "로컬 주소와 기타 알려진 직접 경로만 시도됩니다. NAT를 통한 직접 연결이 줄어들 수 있습니다.", + directPeerStunCustomHelp: "쉼표로 구분된 stun: 주소를 입력하세요. STUN은 네트워크 주소를 검색하고 세션 콘텐츠를 전달하지 않습니다.", + directPeerAutomaticRelayDiscovery: "자동으로 조정 피어 검색", + directPeerAutomaticRelayDiscoveryHelp: "조정 피어는 종단 간 직접 연결을 설정하는 데에만 Circuit Relay v2를 사용합니다. 그들은 애플리케이션 트래픽을 전달하지 않습니다. Maka는 최선의 노력을 다해 공용 IPFS 네트워크를 통해 후보를 검색하며, 수동으로 구성된 피어는 계속 선호됩니다.", + directPeerEnable: "활성화 및 추가", + directPeerDisable: "장애를 입히다", + directPeerAddProfile: "데스크탑에 추가", + directPeerActionFailed: "직접 피어 작업 실패", + peerMesh: "피어 메시", + peerMeshHelp: "이 데스크탑 피어에 대한 비공개 메시 멤버십 및 초대를 관리합니다.", + managePeerMesh: "피어 메시 관리", + installedVersion: "버전", + operatingSystem: "체계", + processId: "프로세스 ID", + lastExitCode: "마지막 종료 코드", + stateRoot: "상태 루트", + directoryRoots: "프로젝트 추가용 디렉터리", + directoryRootsDescription: "원격 클라이언트는 이러한 디렉터리에서만 새 프로젝트를 찾아보고 추가할 수 있습니다. 하나를 제거해도 이미 추가된 프로젝트는 삭제되지 않습니다.", + directoryRootsUnavailable: "Desktop에서 이러한 디렉터리를 관리하려면 이 호스트를 업데이트하거나 복구하세요.", + directoryRootsChanged: "이 디렉토리는 다른 곳에서 변경되었습니다.", + directoryRootsChangedDescription: "초안이 보존됩니다. 계속하기 전에 현재 구성을 로드하세요.", + reloadDirectoryRoots: "현재 구성 로드", + noDirectoryRoots: "디렉터리 검색 및 프로젝트 등록이 비활성화되었습니다.", + directoryRootLabel: "표시 이름", + directoryRootPath: "원격 컴퓨터의 절대 경로", + addDirectoryRoot: "디렉토리 추가", + removeDirectoryRoot: "제거하다", + saveDirectoryRoots: "디렉토리 적용", + directoryRootsActiveTasks: "이 호스트에는 아직 실행 중인 작업이 있습니다.", + directoryRootsActiveTasksDescription: "이러한 디렉터리를 적용하려면 안전한 원격 서비스를 다시 시작해야 합니다. 작업은 명시적인 확인 후에만 중단됩니다.", + configureDirectoriesInterrupt: "작업을 중단하고 적용", + refresh: "새로 고치다", + startService: "시작", + restartService: "다시 시작", + restartActiveTasksDescription: "다시 시작하면 현재 작업이 중지됩니다. 중단하고 계속하시겠습니까?", + restartInterrupt: "작업을 중단하고 다시 시작", + repairService: "수리하다", + updateService: "일치하는 버전 설치", + updatePolicy: "정책 업데이트", + updatePolicyDescription: "이 호스트가 따르는 Maka 릴리스를 선택하세요.", + updatePolicyManual: "수동", + updatePolicyAutomatic: "오토매틱", + updatePolicyOptions: { + manual: "수동 업데이트", + fixed: "고정 버전", + latest: "최신 안정 채널", + next: "다음 미리보기 채널" + }, + updatePolicyFixedVersion: "버전", + updatePolicySave: "정책 저장", + updatePolicyCheckNow: "지금 확인하세요", + updatePolicyUnavailable: "자동 업데이트 정책을 사용할 수 없습니다.", + updateSchedulerUnavailable: "이 런타임 호스트에서는 자동 업데이트를 사용할 수 없습니다.", + updateSchedulerUnavailableBody: "수정된 버전이나 출시 채널을 선택하기 전에 서비스를 업데이트하거나 복구하세요.", + updateSchedulerUnsupported: "지원되지 않음", + updateSchedulerInactive: "비활성", + updateSchedulerInactiveBody: "업데이트 스케줄러가 실행되고 있지 않습니다. 자동 업데이트를 활성화하기 전에 서비스를 시작하거나 복구하세요.", + updateSchedulerNeedsRepair: "수리가 필요함", + updateSchedulerNeedsRepairBody: "업데이트 스케줄러가 실행되고 있지 않습니다. 자동 업데이트를 활성화하기 전에 서비스를 복구하세요.", + updatePolicyDisabled: "자동 업데이트가 사용 중지되었습니다.", + updatePolicyActiveTasks: "런타임 호스트가 활성 작업을 소유하므로 이 업데이트가 연기되었습니다.", + updatePolicyNotNewer: (version: string) => `Maka ${version}은(는) 이 호스트보다 최신이 아닙니다.`, + updatePolicyManualAction: (version: string) => `Maka ${version}은 수동 업데이트가 필요합니다`, + updatePolicyManualReason: { + current_compatibility_unknown: "설치된 버전에 알 수 없는 저장소 호환성이 있습니다.", + target_compatibility_unknown: "대상 버전에 알 수 없는 저장소 호환성이 있습니다.", + compatibility_mismatch: "대상에는 수동 스토리지 호환성 결정이 필요합니다." + }, + updatePhase: { + preparing_cli: "로컬 CLI 준비 중…", + checking: "버전 확인 중…", + staging: "새 버전 준비 중…", + retiring: "현재 런타임 호스트를 안전하게 중지하는 중…", + replacing: "새 버전을 시작하고 확인하는 중…" + }, + updateBlockedTitle: "런타임 호스트는 여전히 활성 작업을 소유할 수 있습니다.", + updateBlockedBody: "데스크톱은 현재 호스트가 안전하게 중지될 수 있음을 증명할 수 없습니다. 계속하면 복구 가능한 작업 상태와 해결되지 않은 외부 효과를 유지하면서 현재 실행이 중단됩니다.", + updateInterrupt: "인터럽트 및 업데이트", + updateComplete: (from: string, to: string) => `런타임 호스트가 ${from}에서 ${to}로 업데이트되었습니다.`, + updateRepaired: (version: string) => `런타임 호스트 ${version}이 다시 실행 중입니다.`, + updateAlreadyCurrent: (version: string) => `런타임 호스트가 이미 ${version}에 있습니다.`, + showLogs: "로그 보기", + noLogs: "서비스 로그를 찾을 수 없습니다.", + uninstallService: "서비스 제거", + uninstallConfirmTitle: "이 런타임 호스트를 제거하시겠습니까?", + uninstallConfirmBody: "이렇게 하면 Maka 관리 서비스 및 프로그램이 중지되고 제거되며 상태 루트, 프로젝트 및 작업 데이터는 보존됩니다. 데스크탑 프로필은 제거되지 않습니다.", + uninstallConfirm: "서비스 제거", + uninstallRetained: (path: string) => `서비스가 제거되었습니다. 데이터는 ${path}에 보관되었습니다.`, + managementActionFailed: "런타임 호스트 서비스를 관리할 수 없습니다.", + managementReconnectFailed: "변경사항이 적용되었지만 데스크톱에서 다시 연결할 수 없습니다.", + manageAccess: "액세스 관리", + accessTitle: "입장", + noAccessCredentials: "활성 액세스 자격 증명 없음", + currentDesktop: "이 데스크탑", + accessKind: { + owner: "클라이언트 액세스", + capabilityProvider: "기능 제공자" + }, + accessPending: "확인 대기 중", + accessCreated: (date: string) => `${date}을(를) 생성했습니다.`, + rotateCredential: "자격증명 순환", + rotateCredentialConfirmTitle: "이 데스크톱 사용자 인증 정보를 순환하시겠습니까?", + rotateCredentialConfirmBody: "순환은 이 런타임 호스트를 다시 연결하고 활성 작업을 중단할 수 있습니다. 계속하기 전에 활성 작업을 완료하거나 일시 중지하세요.", + rotateCredentialConfirm: "계속 회전", + enableBeforeRotate: "이 데스크탑 자격 증명을 교체하기 전에 이 런타임 호스트를 활성화하십시오.", + startBeforeChangingAccess: "액세스를 변경하기 전에 런타임 호스트 서비스를 시작하십시오.", + revokeCredential: "취소", + revokeCredentialConfirm: (name: string) => `${name}에 대한 액세스를 취소하시겠습니까?`, + revokeCredentialConfirmBody: "이 자격 증명을 사용하는 클라이언트는 즉시 연결이 끊어지며 이로 인해 활성 작업이 중단될 수 있습니다.", + accessActionFailed: "액세스를 관리할 수 없습니다.", + back: "뒤쪽에", + remove: "제거하다", + empty: "아직 원격 호스트가 없습니다.", + loadFailed: "런타임 호스트 프로필을 로드할 수 없습니다.", + selectFailed: "런타임 호스트를 업데이트할 수 없습니다.", + saveFailed: "런타임 호스트 프로필을 저장할 수 없습니다.", + removeFailed: "런타임 호스트 프로필을 제거할 수 없습니다.", + pairingRecoveryTitle: "페어링이 완료되지 않았습니다.", + pairingRecoveryDescription: "영향을 받은 호스트 메뉴에서 다시 시도하거나 페어링을 취소하여 완료되지 않은 연결을 정리하십시오.", + resolvePairingRecovery: "페어링 재시도", + resolvePairingRecoveryFailed: "페어링 복구를 해결할 수 없습니다.", + pairingPendingBadge: "페어링이 완료되지 않았습니다.", + discardPairing: "페어링 취소", + discardPairingConfirmTitle: "이 페어링을 삭제하시겠습니까?", + discardPairingConfirmBody: "완료되지 않은 연결과 로컬에 저장된 임시 자격 증명이 제거됩니다. 새로운 초대로 다시 참여할 수 있습니다.", + discardPairingFailed: "페어링을 삭제할 수 없습니다.", + moreActions: (name: string) => `${name}에 대한 추가 작업` + }, + section: "작업공간", + sectionHelp: "기본 프로젝트에 새 작업이 열립니다. 하나도 없으면 마지막으로 사용한 프로젝트를 재사용합니다. 입력 상자 옆에 있는 작업을 다른 프로젝트로 전환할 수 있습니다.", + addProject: "프로젝트 추가", + defaultBadge: "기본", + setDefault: "기본값으로 설정", + setDefaultTitle: "이 프로젝트에서 새 작업 열기", + setDefaultDisabledTitle: "폴더를 사용할 수 없으므로 기본값이 될 수 없습니다.", + setDefaultFailed: "기본 프로젝트를 설정할 수 없습니다.", + rename: "이름 바꾸기", + renameLabel: "프로젝트 이름", + renameFailed: "프로젝트 이름을 바꿀 수 없습니다.", + openFolder: "프로젝트 폴더 열기", + openFolderFailed: "이 폴더를 열 수 없습니다. 이동되었거나 삭제되었을 수 있습니다.", + save: "구하다", + cancel: "취소", + clearDefault: "기본값 지우기", + remove: "마카에서 삭제", + removeConfirmTitle: "Maka에서 이 프로젝트를 삭제하시겠습니까?", + removeConfirmBody: "이렇게 하면 Maka의 프로젝트 목록에서만 제거됩니다. 디스크의 파일은 그대로 유지됩니다. 이 프로젝트의 작업은 \"그룹 해제\"로 이동되며 삭제되지 않습니다.", + removeConfirm: "제거하다", + removeCancel: "취소", + actionFailed: "작업 실패", + unavailable: "폴더를 사용할 수 없음", + defaultUnavailable: "기본 프로젝트는 더 이상 사용할 수 없으므로 새 작업에서는 마지막으로 사용한 프로젝트를 재사용합니다.", + emptyTitle: "아직 프로젝트가 없습니다.", + emptyBody: "프로젝트 폴더를 추가하면 프로젝트별로 작업을 그룹화하는 사이드바를 사용하여 그 안에서 새 작업을 시작할 수 있습니다.", + moreActions: (projectName: string) => `${projectName}에 대한 추가 작업` + } } satisfies UiCatalog; - export function getSettingsProjectsCopy(locale: UiLocale): SettingsProjectsCopy { return SETTINGS_PROJECTS_COPY_BY_LOCALE[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-shared-copy.ts b/apps/desktop/src/renderer/locales/settings-shared-copy.ts index f414806ed0..bdbd8b642e 100644 --- a/apps/desktop/src/renderer/locales/settings-shared-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-shared-copy.ts @@ -18,7 +18,6 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - export type SettingsSharedCopy = { modalLabel: string; contentLabel: string; @@ -66,8 +65,7 @@ export type SettingsSharedCopy = { reviewScheduleHelp: string; }; }; - -const SETTINGS_SHARED_COPY_BY_LOCALE = { +const SETTINGS_SHARED_COPY_BY_LOCALE_BASE = { zh: { modalLabel: '设置', contentLabel: '设置内容', @@ -105,8 +103,8 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { dataLocation: '数据位置', dataLocationHelp: '任务、设置、使用统计与凭据都以文件形式存放在本机的这个位置。', reviewSchedule: '回顾计划', - reviewScheduleHelp: '每日回顾的生成时间与使用的模型。', - }, + reviewScheduleHelp: '每日回顾的生成时间与使用的模型。' + } }, en: { modalLabel: 'Settings', @@ -145,11 +143,53 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { dataLocation: 'Data location', dataLocationHelp: 'Tasks, settings, usage statistics, and credentials are stored as files in this location on your machine.', reviewSchedule: 'Review schedule', - reviewScheduleHelp: 'When the daily review runs, and which model writes it.', - }, - }, + reviewScheduleHelp: 'When the daily review runs, and which model writes it.' + } + } +} satisfies Omit, 'ko'>; +const SETTINGS_SHARED_COPY_BY_LOCALE = { + ...SETTINGS_SHARED_COPY_BY_LOCALE_BASE, + ko: { + modalLabel: "설정", + contentLabel: "설정 내용", + sidebarLabel: "설정 사이드바", + navigationLabel: "설정 섹션", + backToApp: "앱으로 돌아가기", + close: "닫다", + loading: "설정 로드 중", + retry: "다시 시도하세요", + save: "구하다", + cancel: "취소", + copy: "복사", + copied: "복사됨", + failed: "실패한", + settingsLoadFailed: "설정을 로드할 수 없습니다.", + usageLoadFailed: "사용 통계를 로드할 수 없습니다.", + runtimeHost: "런타임 호스트", + runtimeHostUnavailable: "이 런타임 호스트를 사용할 수 없습니다. 다른 호스트를 선택하거나 프로젝트에서 연결을 다시 시도하세요.", + unknownError: "문제가 발생했습니다. 다시 시도해 보세요.", + unavailablePage: "이 페이지는 Maka 설정 트리의 일부이며 런타임 기능으로 활성화됩니다.", + showDetails: "세부정보 표시", + hideDetails: "세부정보 숨기기", + ready: "준비가 된", + groups: { + memorySources: "메모리", + memorySourcesHelp: "Maka는 귀하가 채팅에서 확인한 정보를 기억하고 이후 답변에 사용합니다.", + memoryDocument: "메모리 파일 및 백업", + memoryDocumentHelp: "메모리는 로컬 MEMORY.md에 있습니다. 여기에서 원시 파일을 편집하거나 백업을 복원하세요.", + memoryEntries: "마카가 기억하는 것", + memoryEntriesHelp: "항목을 필터링하거나, 수동으로 추가하거나, 더 이상 필요하지 않은 항목을 보관하세요.", + searchProvider: "검색 공급자", + searchProviderHelp: "공급자 및 자격 증명 웹 검색이 사용됩니다.", + searchBehavior: "검색 행동", + searchBehaviorHelp: "검색이 실행되는 시기 및 반환되는 결과 수.", + dataLocation: "데이터 위치", + dataLocationHelp: "작업, 설정, 사용 통계 및 자격 증명은 컴퓨터의 이 위치에 파일로 저장됩니다.", + reviewSchedule: "일정 검토", + reviewScheduleHelp: "일일 리뷰가 실행되는 시기와 이를 작성하는 모델." + } + } } satisfies UiCatalog; - export function getSettingsSharedCopy(locale: UiLocale): SettingsSharedCopy { return SETTINGS_SHARED_COPY_BY_LOCALE[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts index 6241da520e..59979027c8 100644 --- a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts @@ -18,16 +18,12 @@ */ import type { SubagentProfile } from '@maka/core/subagent-settings'; - import type { ThinkingLevel } from '@maka/core/model-thinking'; - import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - type ProfileCopy = { label: string; description: string; }; - export type SubagentSettingsCopy = { section: { title: string; @@ -98,26 +94,25 @@ export type SubagentSettingsCopy = { profiles: Record; thinking: Record; }; - -const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { +const SETTINGS_SUBAGENTS_COPY_BY_LOCALE_BASE = { zh: { section: { title: '已批准的子 Agent', - count: (total) => `共 ${total} 个配置`, + count: total => `共 ${total} 个配置`, add: '添加子 Agent', emptyTitle: '还没有子 Agent 配置', - emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。', + emptyDescription: '添加一个配置后,主 Agent 就能把合适的任务交给独立模型处理。' }, row: { enabled: '启用', - configure: (name) => `配置“${name}”`, - fallbackDescription: '尚未填写适用场景', + configure: name => `配置“${name}”`, + fallbackDescription: '尚未填写适用场景' }, status: { missingConnection: '连接不存在', providerRetired: '登录方式已移除 · 请改用其他连接', connectionDisabled: '连接已停用', - modelDisabled: '模型未启用', + modelDisabled: '模型未启用' }, editor: { backToList: '返回子 Agent 列表', @@ -148,29 +143,38 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { noConnection: '请先在“模型”页启用一个模型连接。', noModel: '所选连接没有已启用的模型。', requiredName: '请输入显示名称。', - invalidId: (max) => `只能使用字母、数字、点、下划线、冒号和连字符,最多 ${max} 个字符。`, + invalidId: max => `只能使用字母、数字、点、下划线、冒号和连字符,最多 ${max} 个字符。`, duplicateId: '这个 subagent_id 已经存在。', invalidConnection: '请选择一个已启用的模型连接。', invalidModel: '请选择一个已启用的模型。', cancel: '取消', create: '创建', save: '保存', - saving: '保存中…', + saving: '保存中…' }, remove: { - title: (name) => `删除“${name}”?`, + title: name => `删除“${name}”?`, description: '主 Agent 将不再看到这个配置。已创建的子任务不会被删除。', confirm: '删除', - cancel: '取消', + cancel: '取消' }, toast: { saveFailed: '保存子 Agent 配置失败', - rejected: '配置没有被保存。请确认名称长度和配置数量都在上限之内。', + rejected: '配置没有被保存。请确认名称长度和配置数量都在上限之内。' }, profiles: { - local_read: { label: '代码阅读', description: '只读访问当前工作区,适合搜索、理解和总结代码。' }, - web_research: { label: '网络研究', description: '只使用联网搜索,适合查找外部资料和最新信息。' }, - implementation: { label: '实现代码', description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' }, + local_read: { + label: '代码阅读', + description: '只读访问当前工作区,适合搜索、理解和总结代码。' + }, + web_research: { + label: '网络研究', + description: '只使用联网搜索,适合查找外部资料和最新信息。' + }, + implementation: { + label: '实现代码', + description: '可以读写文件并执行命令,在隔离 worktree 中完成改动。' + } }, thinking: { off: '关闭', @@ -179,27 +183,27 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { medium: '中', high: '高', xhigh: '超高', - max: '最大', - }, + max: '最大' + } }, en: { section: { title: 'Approved subagents', - count: (total) => `${total} presets`, + count: total => `${total} presets`, add: 'Add subagent', emptyTitle: 'No subagent presets yet', - emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.', + emptyDescription: 'Add a preset so the main agent can delegate suitable work to a separate model.' }, row: { enabled: 'Enabled', - configure: (name) => `Configure “${name}”`, - fallbackDescription: 'No usage guidance yet', + configure: name => `Configure “${name}”`, + fallbackDescription: 'No usage guidance yet' }, status: { missingConnection: 'Connection missing', providerRetired: 'Sign-in retired · route to another connection', connectionDisabled: 'Connection disabled', - modelDisabled: 'Model not enabled', + modelDisabled: 'Model not enabled' }, editor: { backToList: 'Back to subagents', @@ -230,29 +234,38 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { noConnection: 'Enable a model connection on the Models page first.', noModel: 'The selected connection has no enabled models.', requiredName: 'Enter a display name.', - invalidId: (max) => `Use only letters, numbers, dots, underscores, colons, and hyphens, up to ${max} characters.`, + invalidId: max => `Use only letters, numbers, dots, underscores, colons, and hyphens, up to ${max} characters.`, duplicateId: 'That subagent_id already exists.', invalidConnection: 'Select an enabled model connection.', invalidModel: 'Select an enabled model.', cancel: 'Cancel', create: 'Create', save: 'Save', - saving: 'Saving…', + saving: 'Saving…' }, remove: { - title: (name) => `Remove “${name}”?`, + title: name => `Remove “${name}”?`, description: 'The main agent will no longer see this preset. Existing child tasks are not deleted.', confirm: 'Remove', - cancel: 'Cancel', + cancel: 'Cancel' }, toast: { saveFailed: 'Failed to save subagent presets', - rejected: 'The preset was not saved. Check that its name length and the preset count are within their limits.', + rejected: 'The preset was not saved. Check that its name length and the preset count are within their limits.' }, profiles: { - local_read: { label: 'Code reading', description: 'Read-only access to the current workspace for search, understanding, and summaries.' }, - web_research: { label: 'Web research', description: 'Web search only, for external sources and current information.' }, - implementation: { label: 'Implementation', description: 'Read and write files and run commands in an isolated worktree.' }, + local_read: { + label: 'Code reading', + description: 'Read-only access to the current workspace for search, understanding, and summaries.' + }, + web_research: { + label: 'Web research', + description: 'Web search only, for external sources and current information.' + }, + implementation: { + label: 'Implementation', + description: 'Read and write files and run commands in an isolated worktree.' + } }, thinking: { off: 'Off', @@ -261,11 +274,104 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { medium: 'Medium', high: 'High', xhigh: 'Extra high', - max: 'Maximum', + max: 'Maximum' + } + } +} satisfies Omit, 'ko'>; +const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { + ...SETTINGS_SUBAGENTS_COPY_BY_LOCALE_BASE, + ko: { + section: { + title: "승인된 하위 에이전트", + count: total => `${total} 사전 설정`, + add: "하위 에이전트 추가", + emptyTitle: "아직 하위 에이전트 사전 설정이 없습니다.", + emptyDescription: "주체가 적절한 작업을 별도의 모델에 위임할 수 있도록 사전 설정을 추가합니다." }, - }, + row: { + enabled: "활성화됨", + configure: name => `"${name}" 구성`, + fallbackDescription: "아직 이용안내가 없습니다." + }, + status: { + missingConnection: "연결이 누락되었습니다.", + providerRetired: "로그인이 종료됨 · 다른 연결로 라우팅", + connectionDisabled: "연결이 비활성화되었습니다.", + modelDisabled: "모델이 활성화되지 않았습니다." + }, + editor: { + backToList: "하위 에이전트로 돌아가기", + createSubtitle: "주체가 자동으로 선택할 수 있는 모델 프리셋을 생성합니다.", + editSubtitle: "사용 지침, 기능 경계 및 모델 경로를 변경합니다.", + groupPurpose: "목적", + groupPurposeHelp: "주체는 주로 여기의 이름과 안내에서 프리셋을 선택합니다.", + groupRoute: "기능 및 모델", + groupRouteHelp: "이 하위 에이전트가 수행할 수 있는 작업과 실행되는 모델을 수정합니다.", + dangerZone: "하위 에이전트 제거", + dangerZoneHelp: "이 작업은 취소할 수 없습니다.", + delete: "제거하다", + enabled: "활성화됨", + enabledDescription: "주 상담원이 선택하지 않고 프리셋을 유지하려면 이 기능을 끄세요.", + name: "표시 이름", + namePlaceholder: "빠른 코드 리더", + id: 'subagent_id', + idDescription: "생성 후 안정적입니다. 기본 에이전트와 작업 기록은 이를 사용하여 이 사전 설정을 식별합니다.", + idPlaceholder: 'fast-reader', + description: "언제 사용하나요?", + descriptionPlaceholder: "대규모 저장소를 빠르고 저렴하게 탐색", + profile: "기능 프로필", + connection: "모델 연결", + model: "모델", + thinking: "사고 수준", + defaultThinking: "모델 기본값 사용", + implementationWarning: "구현 프로필은 격리된 작업 트리 내에서 파일을 작성하고 명령을 실행할 수 있습니다.", + noConnection: "먼저 모델 페이지에서 모델 연결을 활성화하세요.", + noModel: "선택한 연결에는 활성화된 모델이 없습니다.", + requiredName: "표시 이름을 입력하세요.", + invalidId: max => `최대 ${max}자까지 문자, 숫자, 점, 밑줄, 콜론, 하이픈만 사용하세요.`, + duplicateId: "해당 subagent_id가 이미 존재합니다.", + invalidConnection: "활성화된 모델 연결을 선택합니다.", + invalidModel: "활성화된 모델을 선택하세요.", + cancel: "취소", + create: "만들다", + save: "구하다", + saving: "절약…" + }, + remove: { + title: name => `'${name}'을 삭제하시겠습니까?`, + description: "주 상담원은 더 이상 이 사전 설정을 볼 수 없습니다. 기존 하위 작업은 삭제되지 않습니다.", + confirm: "제거하다", + cancel: "취소" + }, + toast: { + saveFailed: "하위 에이전트 사전 설정을 저장하지 못했습니다.", + rejected: "사전 설정이 저장되지 않았습니다. 이름 길이와 사전 설정 개수가 한도 내에 있는지 확인하세요." + }, + profiles: { + local_read: { + label: "코드 판독", + description: "검색, 이해 및 요약을 위한 현재 작업공간에 대한 읽기 전용 액세스입니다." + }, + web_research: { + label: "웹 조사", + description: "외부 소스 및 최신 정보에 대한 웹 검색만 가능합니다." + }, + implementation: { + label: "구현", + description: "격리된 작업 트리에서 파일을 읽고 쓰고 명령을 실행합니다." + } + }, + thinking: { + off: "끄다", + minimal: "최소한의", + low: "낮은", + medium: "중간", + high: "높은", + xhigh: "매우 높음", + max: "최고" + } + } } satisfies UiCatalog; - export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy { return SETTINGS_SUBAGENTS_COPY_BY_LOCALE[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-tasks-copy.ts b/apps/desktop/src/renderer/locales/settings-tasks-copy.ts index 4bd7d41e33..f1b56b2e19 100644 --- a/apps/desktop/src/renderer/locales/settings-tasks-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-tasks-copy.ts @@ -18,7 +18,6 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - /** * Restoring and deleting a single task speak through the rail's own row * actions, so their confirms and toasts are not repeated here. What is left is @@ -58,8 +57,7 @@ export type SettingsTasksCopy = { emptyTitle: string; emptyBody: string; }; - -const SETTINGS_TASKS_COPY_BY_LOCALE = { +const SETTINGS_TASKS_COPY_BY_LOCALE_BASE = { zh: { listAria: '已归档任务', noProject: '无项目', @@ -85,7 +83,7 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = { restoreTask: (name: string) => `恢复「${name}」`, delete: '彻底删除', emptyTitle: '没有已归档的任务', - emptyBody: '在侧栏里归档一个任务后,可以在这里恢复或彻底删除它。', + emptyBody: '在侧栏里归档一个任务后,可以在这里恢复或彻底删除它。' }, en: { listAria: 'Archived tasks', @@ -93,25 +91,17 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = { deletedParent: 'Parent task deleted', searchLabel: 'Search archived tasks', purgeAll: 'Clear all', - purgeMatches: (count: number) => (count === 1 ? 'Delete this 1' : `Delete these ${count}`), - purgeAllConfirmTitle: (count: number) => - count === 1 ? 'Clear the 1 archived task?' : `Clear all ${count} archived tasks?`, - purgeMatchesConfirmTitle: (count: number) => - count === 1 ? 'Delete the 1 task you searched for?' : `Delete the ${count} tasks you searched for?`, - purgeConfirmBody: - 'The tasks and all of their messages are removed permanently. This cannot be undone.', + purgeMatches: (count: number) => count === 1 ? 'Delete this 1' : `Delete these ${count}`, + purgeAllConfirmTitle: (count: number) => count === 1 ? 'Clear the 1 archived task?' : `Clear all ${count} archived tasks?`, + purgeMatchesConfirmTitle: (count: number) => count === 1 ? 'Delete the 1 task you searched for?' : `Delete the ${count} tasks you searched for?`, + purgeConfirmBody: 'The tasks and all of their messages are removed permanently. This cannot be undone.', purgeSubtaskNote: 'Any ordinary subtasks are kept and moved to Archived.', purgeConfirmAction: 'Delete permanently', - purgedToast: (count: number) => (count === 1 ? 'Deleted 1 task' : `Deleted ${count} tasks`), - purgedSubtaskNote: (count: number) => - count === 1 ? '1 subtask moved to Archived' : `${count} subtasks moved to Archived`, - purgeKeptRestored: (count: number) => - count === 1 - ? '1 more was restored meanwhile and kept.' - : `${count} more were restored meanwhile and kept.`, + purgedToast: (count: number) => count === 1 ? 'Deleted 1 task' : `Deleted ${count} tasks`, + purgedSubtaskNote: (count: number) => count === 1 ? '1 subtask moved to Archived' : `${count} subtasks moved to Archived`, + purgeKeptRestored: (count: number) => count === 1 ? '1 more was restored meanwhile and kept.' : `${count} more were restored meanwhile and kept.`, purgeFailedTitle: 'Could not delete the tasks', - purgeFailedBody: (count: number) => - count === 1 ? '1 task is still there. Try again.' : `${count} tasks are still there. Try again.`, + purgeFailedBody: (count: number) => count === 1 ? '1 task is still there. Try again.' : `${count} tasks are still there. Try again.`, purgeUnverified: 'The tasks were deleted, but the list could not be read back to confirm. Reopen this page to check.', noMatchTitle: 'No matching tasks', noMatchBody: 'Try a different search.', @@ -120,10 +110,39 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = { restoreTask: (name: string) => `Restore ${name}`, delete: 'Delete', emptyTitle: 'Nothing archived', - emptyBody: 'Archive a task from the rail to restore or permanently delete it here.', - }, + emptyBody: 'Archive a task from the rail to restore or permanently delete it here.' + } +} satisfies Omit, 'ko'>; +const SETTINGS_TASKS_COPY_BY_LOCALE = { + ...SETTINGS_TASKS_COPY_BY_LOCALE_BASE, + ko: { + listAria: "보관된 작업", + noProject: "프로젝트 없음", + deletedParent: "상위 작업이 삭제되었습니다.", + searchLabel: "보관된 작업 검색", + purgeAll: "모두 지우기", + purgeMatches: (count: number) => count === 1 ? "삭제 1" : `이 ${count} 삭제`, + purgeAllConfirmTitle: (count: number) => count === 1 ? "보관된 작업 1개를 삭제하시겠습니까?" : `${count} 보관된 작업을 모두 지우시겠습니까?`, + purgeMatchesConfirmTitle: (count: number) => count === 1 ? "검색한 작업 1개를 삭제하시겠습니까?" : `검색한 ${count} 작업을 삭제하시겠습니까?`, + purgeConfirmBody: "작업과 해당 메시지가 모두 영구적으로 제거됩니다. 이 작업은 취소할 수 없습니다.", + purgeSubtaskNote: "일반적인 하위 작업은 보관되고 보관됨으로 이동됩니다.", + purgeConfirmAction: "영구 삭제", + purgedToast: (count: number) => count === 1 ? "할 일 1개 삭제됨" : `${count} 작업을 삭제했습니다.`, + purgedSubtaskNote: (count: number) => count === 1 ? "1개의 하위 작업이 보관됨으로 이동되었습니다." : `${count} 하위 작업이 보관됨으로 이동되었습니다.`, + purgeKeptRestored: (count: number) => count === 1 ? "그 동안 1개를 더 복원하여 보관했습니다." : `그 동안 ${count}개가 더 복원되어 보관되었습니다.`, + purgeFailedTitle: "작업을 삭제할 수 없습니다.", + purgeFailedBody: (count: number) => count === 1 ? "아직 할 일이 1개 남아 있습니다. 다시 시도해 보세요." : `${count} 작업이 아직 남아 있습니다. 다시 시도해 보세요.`, + purgeUnverified: "작업이 삭제되었지만 확인을 위해 목록을 다시 읽을 수 없습니다. 이 페이지를 다시 열어 확인하세요.", + noMatchTitle: "일치하는 작업 없음", + noMatchBody: "다른 검색을 시도해 보세요.", + moreActions: (name: string) => `${name}에 대한 추가 작업`, + restore: "복원하다", + restoreTask: (name: string) => `${name} 복원`, + delete: "삭제", + emptyTitle: "보관된 항목이 없습니다.", + emptyBody: "여기에서 작업을 복원하거나 영구적으로 삭제하려면 레일에서 작업을 보관하세요." + } } satisfies UiCatalog; - export function getSettingsTasksCopy(locale: UiLocale): SettingsTasksCopy { return SETTINGS_TASKS_COPY_BY_LOCALE[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts index 48a4097fb9..cea209272d 100644 --- a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts @@ -18,15 +18,10 @@ */ import type { SettingsTestResult } from '@maka/core/settings'; - import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - type SettingsTestResultCopy = { proxy: { - reachable: ( - endpoint: string | undefined, - location: string | undefined, - ) => string; + reachable: (endpoint: string | undefined, location: string | undefined) => string; disabled: string; configurationMissing: string; credentialMissing: string; @@ -42,79 +37,71 @@ type SettingsTestResultCopy = { connectionFailed: string; }; }; - -const COPY = { +const COPY_BASE = { zh: { proxy: { - reachable: (endpoint, location) => - ["代理配置有效", endpoint, location].filter(Boolean).join(" · "), + reachable: (endpoint, location) => ["代理配置有效", endpoint, location].filter(Boolean).join(" · "), disabled: "请先启用代理服务器,再进行测试。", configurationMissing: "请填写代理服务器地址和端口后再测试。", credentialMissing: "代理认证已开启,请输入代理密码后再测试。", timeout: "代理测试超时,请检查代理服务是否可达。", - httpError: (status) => - status === undefined - ? "代理测试返回了错误响应,请检查代理服务或测试地址。" - : `代理测试返回 HTTP ${status},请检查代理服务或测试地址。`, - unreachable: "代理不可达,请检查服务器地址、端口和认证信息。", + httpError: status => status === undefined ? "代理测试返回了错误响应,请检查代理服务或测试地址。" : `代理测试返回 HTTP ${status},请检查代理服务或测试地址。`, + unreachable: "代理不可达,请检查服务器地址、端口和认证信息。" }, bot: { - credentialsValid: (username) => - username - ? `凭据检查已通过 · ${username}。这不代表消息收发服务已启动。` - : "凭据检查已通过。这不代表消息收发服务已启动。", + credentialsValid: username => username ? `凭据检查已通过 · ${username}。这不代表消息收发服务已启动。` : "凭据检查已通过。这不代表消息收发服务已启动。", tokenMissing: "请填写 Bot Token 后再测试。", tokenInvalid: "Bot Token 无效,请检查后重试。", appCredentialsMissing: "请填写 App ID 和 App Secret 后再测试。", - connectionFailed: "请检查凭据和网络设置后重试。", - }, + connectionFailed: "请检查凭据和网络设置后重试。" + } }, en: { proxy: { - reachable: (endpoint, location) => - ["The proxy is reachable", endpoint, location] - .filter(Boolean) - .join(" · "), + reachable: (endpoint, location) => ["The proxy is reachable", endpoint, location].filter(Boolean).join(" · "), disabled: "Enable the proxy server before testing it.", configurationMissing: "Enter a proxy host and port before testing it.", - credentialMissing: - "Proxy authentication is enabled. Enter a proxy password before testing.", - timeout: - "The proxy test timed out. Check whether the proxy service is reachable.", - httpError: (status) => - status === undefined - ? "The proxy test returned an error response. Check the proxy service and test URL." - : `The proxy test returned HTTP ${status}. Check the proxy service and test URL.`, - unreachable: - "The proxy is unreachable. Check its host, port, and authentication settings.", + credentialMissing: "Proxy authentication is enabled. Enter a proxy password before testing.", + timeout: "The proxy test timed out. Check whether the proxy service is reachable.", + httpError: status => status === undefined ? "The proxy test returned an error response. Check the proxy service and test URL." : `The proxy test returned HTTP ${status}. Check the proxy service and test URL.`, + unreachable: "The proxy is unreachable. Check its host, port, and authentication settings." }, bot: { - credentialsValid: (username) => - username - ? `The credential check passed · ${username}. This does not mean the message listener is running.` - : "The credential check passed. This does not mean the message listener is running.", + credentialsValid: username => username ? `The credential check passed · ${username}. This does not mean the message listener is running.` : "The credential check passed. This does not mean the message listener is running.", tokenMissing: "Enter a Bot Token before testing the connection.", tokenInvalid: "The Bot Token is invalid. Check it and try again.", - appCredentialsMissing: - "Enter an App ID and App Secret before testing the connection.", - connectionFailed: - "Check the credentials and network settings, then try again.", + appCredentialsMissing: "Enter an App ID and App Secret before testing the connection.", + connectionFailed: "Check the credentials and network settings, then try again." + } + } +} satisfies Omit, 'ko'>; +const COPY = { + ...COPY_BASE, + ko: { + proxy: { + reachable: (endpoint, location) => ["프록시에 연결할 수 있습니다.", endpoint, location].filter(Boolean).join(" · "), + disabled: "테스트하기 전에 프록시 서버를 활성화하십시오.", + configurationMissing: "테스트하기 전에 프록시 호스트와 포트를 입력하세요.", + credentialMissing: "프록시 인증이 활성화되었습니다. 테스트하기 전에 프록시 비밀번호를 입력하세요.", + timeout: "프록시 테스트 시간이 초과되었습니다. 프록시 서비스에 연결할 수 있는지 확인하세요.", + httpError: status => status === undefined ? "프록시 테스트에서 오류 응답을 반환했습니다. 프록시 서비스와 테스트 URL을 확인하세요." : `프록시 테스트에서 HTTP ${status}을 반환했습니다. 프록시 서비스와 테스트 URL을 확인하세요.`, + unreachable: "프록시에 연결할 수 없습니다. 호스트, 포트, 인증 설정을 확인하세요." }, - }, + bot: { + credentialsValid: username => username ? `자격 증명 확인을 통과했습니다 · ${username}. 이는 메시지 수신기가 실행 중이라는 의미는 아닙니다.` : "자격 증명 확인이 통과되었습니다. 이는 메시지 수신기가 실행 중이라는 의미는 아닙니다.", + tokenMissing: "연결을 테스트하기 전에 봇 토큰을 입력하세요.", + tokenInvalid: "봇 토큰이 유효하지 않습니다. 확인하고 다시 시도해 보세요.", + appCredentialsMissing: "연결을 테스트하기 전에 앱 ID와 앱 비밀을 입력하세요.", + connectionFailed: "자격 증명과 네트워크 설정을 확인한 후 다시 시도하세요." + } + } } satisfies UiCatalog; - -export function settingsTestResultMessage( - result: SettingsTestResult, - locale: UiLocale, -): string { +export function settingsTestResultMessage(result: SettingsTestResult, locale: UiLocale): string { const copy = COPY[locale]; const status = numberDetail(result, "status"); switch (result.code) { case "proxy_reachable": - return copy.proxy.reachable( - stringDetail(result, "endpoint"), - proxyLocation(result), - ); + return copy.proxy.reachable(stringDetail(result, "endpoint"), proxyLocation(result)); case "proxy_disabled": return copy.proxy.disabled; case "proxy_configuration_missing": @@ -138,40 +125,28 @@ export function settingsTestResultMessage( case "bot_connection_failed": return copy.bot.connectionFailed; default: - return locale === "en" && result.message.trim() - ? result.message - : copy.bot.connectionFailed; + return locale === "en" && result.message.trim() ? result.message : copy.bot.connectionFailed; } } - function proxyLocation(result: SettingsTestResult): string | undefined { const ip = stringDetail(result, "ip"); if (!ip) return undefined; const flag = stringDetail(result, "countryFlag"); return flag ? `${flag} ${ip}` : ip; } - function identityUsername(result: SettingsTestResult): string | undefined { const identity = result.details?.identity; if (!identity || typeof identity !== "object") return undefined; - const username = (identity as { username?: unknown }).username; + const username = (identity as { + username?: unknown; + }).username; return typeof username === "string" && username.trim() ? username : undefined; } - -function stringDetail( - result: SettingsTestResult, - key: string, -): string | undefined { +function stringDetail(result: SettingsTestResult, key: string): string | undefined { const value = result.details?.[key]; return typeof value === "string" && value.trim() ? value : undefined; } - -function numberDetail( - result: SettingsTestResult, - key: string, -): number | undefined { +function numberDetail(result: SettingsTestResult, key: string): number | undefined { const value = result.details?.[key]; - return typeof value === "number" && Number.isFinite(value) - ? value - : undefined; + return typeof value === "number" && Number.isFinite(value) ? value : undefined; } diff --git a/apps/desktop/src/renderer/locales/settings-usage-copy.ts b/apps/desktop/src/renderer/locales/settings-usage-copy.ts index f5160b4e2c..7b67a96c38 100644 --- a/apps/desktop/src/renderer/locales/settings-usage-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-usage-copy.ts @@ -18,71 +18,263 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - export type UsageSettingsCopy = { - saveFailed: string; toolbarAria: string; rangeAria: string; ranges: readonly [string, string, string, string]; - refreshingAria: string; refreshAria: string; summaryAria: string; totalRequests: string; totalCost: string; costHelp: string; - totalTokens: string; tokenDetail(input: number, output: number): string; cacheTokens: string; cacheDetail(miss: number, read: number, creation: number): string; - viewAria: string; tabs: readonly [string, string, string, string, string]; filtersAria: string; filterPlaceholder: string; filterAria: string; - statusAria: string; statuses: readonly [string, string, string, string]; details: string; detailsAria: string; recordCount(count: number): string; clearFilters: string; - summaryOnly: string; showDetails: string; filteredEmpty: string; filteredEmptyHelp: string; requestEmpty: string; - costUnavailable: string; incompleteTitle: string; incompleteBody: string; + saveFailed: string; + toolbarAria: string; + rangeAria: string; + ranges: readonly [string, string, string, string]; + refreshingAria: string; + refreshAria: string; + summaryAria: string; + totalRequests: string; + totalCost: string; + costHelp: string; + totalTokens: string; + tokenDetail(input: number, output: number): string; + cacheTokens: string; + cacheDetail(miss: number, read: number, creation: number): string; + viewAria: string; + tabs: readonly [string, string, string, string, string]; + filtersAria: string; + filterPlaceholder: string; + filterAria: string; + statusAria: string; + statuses: readonly [string, string, string, string]; + details: string; + detailsAria: string; + recordCount(count: number): string; + clearFilters: string; + summaryOnly: string; + showDetails: string; + filteredEmpty: string; + filteredEmptyHelp: string; + requestEmpty: string; + costUnavailable: string; + incompleteTitle: string; + incompleteBody: string; tables: { - providersAria: string; modelsAria: string; toolsAria: string; pricingAria: string; requestsAria: string; - providerHeaders: string[]; modelHeaders: string[]; toolHeaders: string[]; pricingHeaders: string[]; requestHeaders: string[]; - noPricing: string; modelKind: string; toolKind: string; unknown: string; untitledSession: string; openSession(label: string): string; success: string; error: string; aborted: string; - providerEmptyTitle: string; providerEmptyBody: string; modelEmptyTitle: string; modelEmptyBody: string; - toolEmptyTitle: string; toolEmptyBody: string; pricingEmptyBody: string; + providersAria: string; + modelsAria: string; + toolsAria: string; + pricingAria: string; + requestsAria: string; + providerHeaders: string[]; + modelHeaders: string[]; + toolHeaders: string[]; + pricingHeaders: string[]; + requestHeaders: string[]; + noPricing: string; + modelKind: string; + toolKind: string; + unknown: string; + untitledSession: string; + openSession(label: string): string; + success: string; + error: string; + aborted: string; + providerEmptyTitle: string; + providerEmptyBody: string; + modelEmptyTitle: string; + modelEmptyBody: string; + toolEmptyTitle: string; + toolEmptyBody: string; + pricingEmptyBody: string; }; }; - -const SETTINGS_USAGE_COPY = { +const SETTINGS_USAGE_COPY_BASE = { zh: { - saveFailed: '保存使用统计设置失败', toolbarAria: '使用统计范围与刷新', rangeAria: '使用统计时间范围', ranges: ['24h', '7天', '30天', '全部'], - refreshingAria: '正在刷新使用统计', refreshAria: '刷新使用统计', summaryAria: '使用统计汇总指标', totalRequests: '模型调用', totalCost: '总费用', costHelp: '以模型供应商最终结算为准', - totalTokens: '总 Token', tokenDetail: (input, output) => `输入 ${input} / 输出 ${output}`, cacheTokens: '缓存 Token', - cacheDetail: (miss, read, creation) => `新 ${miss} / 命中 ${read} / 创建 ${creation}`, viewAria: '使用统计视图', tabs: ['活动记录', '供应商统计', '模型统计', '工具统计', '定价配置'], - filtersAria: '活动记录筛选', filterPlaceholder: '按模型或工具筛选…', filterAria: '按模型或工具筛选活动记录', statusAria: '活动状态筛选', - statuses: ['全部状态', '成功', '错误', '已中止'], details: '详情记录', detailsAria: '显示使用统计详情记录', recordCount: (count) => `共 ${count} 条记录`, clearFilters: '清除筛选', + saveFailed: '保存使用统计设置失败', + toolbarAria: '使用统计范围与刷新', + rangeAria: '使用统计时间范围', + ranges: ['24h', '7天', '30天', '全部'], + refreshingAria: '正在刷新使用统计', + refreshAria: '刷新使用统计', + summaryAria: '使用统计汇总指标', + totalRequests: '模型调用', + totalCost: '总费用', + costHelp: '以模型供应商最终结算为准', + totalTokens: '总 Token', + tokenDetail: (input, output) => `输入 ${input} / 输出 ${output}`, + cacheTokens: '缓存 Token', + cacheDetail: (miss, read, creation) => `新 ${miss} / 命中 ${read} / 创建 ${creation}`, + viewAria: '使用统计视图', + tabs: ['活动记录', '供应商统计', '模型统计', '工具统计', '定价配置'], + filtersAria: '活动记录筛选', + filterPlaceholder: '按模型或工具筛选…', + filterAria: '按模型或工具筛选活动记录', + statusAria: '活动状态筛选', + statuses: ['全部状态', '成功', '错误', '已中止'], + details: '详情记录', + detailsAria: '显示使用统计详情记录', + recordCount: count => `共 ${count} 条记录`, + clearFilters: '清除筛选', summaryOnly: '当前仅显示汇总指标。打开详情记录后,可以查看逐条模型调用和工具调用,按模型、工具或状态筛选,并用于排查费用与失败调用。', - showDetails: '显示明细', filteredEmpty: '没有符合筛选条件的活动记录', filteredEmptyHelp: '调整或清除筛选条件后可查看全部活动记录。', requestEmpty: '暂无活动记录', - costUnavailable: '费用未知', incompleteTitle: '统计可能不完整', + showDetails: '显示明细', + filteredEmpty: '没有符合筛选条件的活动记录', + filteredEmptyHelp: '调整或清除筛选条件后可查看全部活动记录。', + requestEmpty: '暂无活动记录', + costUnavailable: '费用未知', + incompleteTitle: '统计可能不完整', incompleteBody: '部分记录未能读取、尚未纳入统计或超出展示上限,实际用量可能高于此处显示。', tables: { - providersAria: '使用统计供应商统计表', modelsAria: '使用统计模型统计表', toolsAria: '使用统计工具统计表', pricingAria: '使用统计定价配置表', requestsAria: '使用统计活动记录表', - providerHeaders: ['供应商', '调用', 'Token', '费用'], modelHeaders: ['模型', '调用', 'Token', '费用'], toolHeaders: ['工具', '调用', '成功', '错误', '平均耗时'], - pricingHeaders: ['供应商', '模型', '输入 / 1M', '输出 / 1M'], requestHeaders: ['时间', '类型', '对象', '任务', 'Token', '费用', '延迟', '状态'], - noPricing: '暂无定价覆盖配置', modelKind: '模型', toolKind: '工具', unknown: '未知', untitledSession: '未命名会话', openSession: (label) => `打开会话「${label}」`, success: '成功', error: '错误', aborted: '已中止', - providerEmptyTitle: '暂无供应商用量', providerEmptyBody: '完成一次模型调用后,这里会按供应商聚合调用数、Token 与费用。', - modelEmptyTitle: '暂无模型用量', modelEmptyBody: '完成一次模型调用后,这里会按模型聚合调用数、Token 与费用。', - toolEmptyTitle: '暂无工具调用', toolEmptyBody: '智能体调用工具后,这里会按工具聚合调用次数、成功、错误与平均耗时。', - pricingEmptyBody: '未配置定价覆盖时,费用按内置模型定价表结算;在此可为特定模型登记自定义价格。', - }, + providersAria: '使用统计供应商统计表', + modelsAria: '使用统计模型统计表', + toolsAria: '使用统计工具统计表', + pricingAria: '使用统计定价配置表', + requestsAria: '使用统计活动记录表', + providerHeaders: ['供应商', '调用', 'Token', '费用'], + modelHeaders: ['模型', '调用', 'Token', '费用'], + toolHeaders: ['工具', '调用', '成功', '错误', '平均耗时'], + pricingHeaders: ['供应商', '模型', '输入 / 1M', '输出 / 1M'], + requestHeaders: ['时间', '类型', '对象', '任务', 'Token', '费用', '延迟', '状态'], + noPricing: '暂无定价覆盖配置', + modelKind: '模型', + toolKind: '工具', + unknown: '未知', + untitledSession: '未命名会话', + openSession: label => `打开会话「${label}」`, + success: '成功', + error: '错误', + aborted: '已中止', + providerEmptyTitle: '暂无供应商用量', + providerEmptyBody: '完成一次模型调用后,这里会按供应商聚合调用数、Token 与费用。', + modelEmptyTitle: '暂无模型用量', + modelEmptyBody: '完成一次模型调用后,这里会按模型聚合调用数、Token 与费用。', + toolEmptyTitle: '暂无工具调用', + toolEmptyBody: '智能体调用工具后,这里会按工具聚合调用次数、成功、错误与平均耗时。', + pricingEmptyBody: '未配置定价覆盖时,费用按内置模型定价表结算;在此可为特定模型登记自定义价格。' + } }, en: { - saveFailed: 'Failed to save usage settings', toolbarAria: 'Usage range and refresh', rangeAria: 'Usage time range', ranges: ['24h', '7 days', '30 days', 'All'], - refreshingAria: 'Refreshing usage', refreshAria: 'Refresh usage', summaryAria: 'Usage summary metrics', totalRequests: 'Model calls', totalCost: 'Total cost', costHelp: 'Final billing is determined by the model provider', - totalTokens: 'Total tokens', tokenDetail: (input, output) => `Input ${input} / output ${output}`, cacheTokens: 'Cache tokens', - cacheDetail: (miss, read, creation) => `New ${miss} / hit ${read} / created ${creation}`, viewAria: 'Usage view', tabs: ['Activity log', 'Providers', 'Models', 'Tools', 'Pricing'], - filtersAria: 'Activity filters', filterPlaceholder: 'Filter by model or tool…', filterAria: 'Filter activity by model or tool', statusAria: 'Filter by activity status', - statuses: ['All statuses', 'Success', 'Error', 'Aborted'], details: 'Detailed records', detailsAria: 'Show detailed usage records', recordCount: (count) => `${count} ${count === 1 ? 'record' : 'records'}`, clearFilters: 'Clear filters', + saveFailed: 'Failed to save usage settings', + toolbarAria: 'Usage range and refresh', + rangeAria: 'Usage time range', + ranges: ['24h', '7 days', '30 days', 'All'], + refreshingAria: 'Refreshing usage', + refreshAria: 'Refresh usage', + summaryAria: 'Usage summary metrics', + totalRequests: 'Model calls', + totalCost: 'Total cost', + costHelp: 'Final billing is determined by the model provider', + totalTokens: 'Total tokens', + tokenDetail: (input, output) => `Input ${input} / output ${output}`, + cacheTokens: 'Cache tokens', + cacheDetail: (miss, read, creation) => `New ${miss} / hit ${read} / created ${creation}`, + viewAria: 'Usage view', + tabs: ['Activity log', 'Providers', 'Models', 'Tools', 'Pricing'], + filtersAria: 'Activity filters', + filterPlaceholder: 'Filter by model or tool…', + filterAria: 'Filter activity by model or tool', + statusAria: 'Filter by activity status', + statuses: ['All statuses', 'Success', 'Error', 'Aborted'], + details: 'Detailed records', + detailsAria: 'Show detailed usage records', + recordCount: count => `${count}개 기록`, + clearFilters: 'Clear filters', summaryOnly: 'Only summary metrics are shown. Enable detailed records to inspect individual model calls and tool calls, filter by model, tool, or status, and investigate costs or failures.', - showDetails: 'Show details', filteredEmpty: 'No activity matches these filters', filteredEmptyHelp: 'Adjust or clear the filters to see all activity records.', requestEmpty: 'No activity records', - costUnavailable: 'Cost unavailable', incompleteTitle: 'These numbers may be incomplete', + showDetails: 'Show details', + filteredEmpty: 'No activity matches these filters', + filteredEmptyHelp: 'Adjust or clear the filters to see all activity records.', + requestEmpty: 'No activity records', + costUnavailable: 'Cost unavailable', + incompleteTitle: 'These numbers may be incomplete', incompleteBody: 'Some records could not be read, are not folded in yet, or exceed the display limit, so real usage may be higher than shown.', tables: { - providersAria: 'Usage by provider', modelsAria: 'Usage by model', toolsAria: 'Usage by tool', pricingAria: 'Usage pricing configuration', requestsAria: 'Usage activity log', - providerHeaders: ['Provider', 'Calls', 'Tokens', 'Cost'], modelHeaders: ['Model', 'Calls', 'Tokens', 'Cost'], toolHeaders: ['Tool', 'Calls', 'Success', 'Errors', 'Average duration'], - pricingHeaders: ['Provider', 'Model', 'Input / 1M', 'Output / 1M'], requestHeaders: ['Time', 'Type', 'Target', 'Task', 'Tokens', 'Cost', 'Latency', 'Status'], - noPricing: 'No pricing overrides', modelKind: 'Model', toolKind: 'Tool', unknown: 'Unknown', untitledSession: 'Untitled session', openSession: (label) => `Open session "${label}"`, success: 'Success', error: 'Error', aborted: 'Aborted', - providerEmptyTitle: 'No provider usage', providerEmptyBody: 'After a model call, provider call counts, tokens, and costs appear here.', - modelEmptyTitle: 'No model usage', modelEmptyBody: 'After a model call, call counts, tokens, and costs appear here by model.', - toolEmptyTitle: 'No tool calls', toolEmptyBody: 'After an agent calls a tool, calls, successes, errors, and average duration appear here by tool.', - pricingEmptyBody: 'Without pricing overrides, costs use the built-in model pricing table. Add custom prices here for specific models.', - }, - }, + providersAria: 'Usage by provider', + modelsAria: 'Usage by model', + toolsAria: 'Usage by tool', + pricingAria: 'Usage pricing configuration', + requestsAria: 'Usage activity log', + providerHeaders: ['Provider', 'Calls', 'Tokens', 'Cost'], + modelHeaders: ['Model', 'Calls', 'Tokens', 'Cost'], + toolHeaders: ['Tool', 'Calls', 'Success', 'Errors', 'Average duration'], + pricingHeaders: ['Provider', 'Model', 'Input / 1M', 'Output / 1M'], + requestHeaders: ['Time', 'Type', 'Target', 'Task', 'Tokens', 'Cost', 'Latency', 'Status'], + noPricing: 'No pricing overrides', + modelKind: 'Model', + toolKind: 'Tool', + unknown: 'Unknown', + untitledSession: 'Untitled session', + openSession: label => `Open session "${label}"`, + success: 'Success', + error: 'Error', + aborted: 'Aborted', + providerEmptyTitle: 'No provider usage', + providerEmptyBody: 'After a model call, provider call counts, tokens, and costs appear here.', + modelEmptyTitle: 'No model usage', + modelEmptyBody: 'After a model call, call counts, tokens, and costs appear here by model.', + toolEmptyTitle: 'No tool calls', + toolEmptyBody: 'After an agent calls a tool, calls, successes, errors, and average duration appear here by tool.', + pricingEmptyBody: 'Without pricing overrides, costs use the built-in model pricing table. Add custom prices here for specific models.' + } + } +} satisfies Omit, 'ko'>; +const SETTINGS_USAGE_COPY = { + ...SETTINGS_USAGE_COPY_BASE, + ko: { + saveFailed: "사용 설정을 저장하지 못했습니다.", + toolbarAria: "사용 범위 및 새로고침", + rangeAria: "사용시간 범위", + ranges: ['24h', "7일", "30일", "모두"], + refreshingAria: "상쾌한 사용법", + refreshAria: "사용량 새로고침", + summaryAria: "사용량 요약 측정항목", + totalRequests: "모델 호출", + totalCost: "총 비용", + costHelp: "최종 청구는 모델 제공자가 결정합니다.", + totalTokens: "총 토큰", + tokenDetail: (input, output) => `입력 ${input} / 출력 ${output}`, + cacheTokens: "캐시 토큰", + cacheDetail: (miss, read, creation) => `새로운 ${miss} / ${read} 조회 / ${creation} 생성`, + viewAria: "사용량 보기", + tabs: ["활동 로그", "공급자", "모델", "도구", "가격"], + filtersAria: "활동 필터", + filterPlaceholder: "모델 또는 도구별로 필터링…", + filterAria: "모델 또는 도구별로 활동 필터링", + statusAria: "활동 상태로 필터링", + statuses: ["모든 상태", "성공", "오류", "중단됨"], + details: "상세한 기록", + detailsAria: "자세한 사용 기록 표시", + recordCount: count => `${count}개 기록`, + clearFilters: "필터 지우기", + summaryOnly: "요약 측정항목만 표시됩니다. 세부 기록을 활성화하여 개별 모델 호출 및 도구 호출을 검사하고, 모델, 도구 또는 상태별로 필터링하고, 비용이나 실패를 조사합니다.", + showDetails: "세부정보 표시", + filteredEmpty: "이 필터와 일치하는 활동이 없습니다.", + filteredEmptyHelp: "모든 활동 기록을 보려면 필터를 조정하거나 지우십시오.", + requestEmpty: "활동 기록 없음", + costUnavailable: "비용을 알 수 없음", + incompleteTitle: "이 숫자는 불완전할 수 있습니다.", + incompleteBody: "일부 기록을 읽을 수 없거나 아직 접히지 않았거나 표시 제한을 초과하여 실제 사용량이 표시된 것보다 높을 수 있습니다.", + tables: { + providersAria: "공급자별 사용량", + modelsAria: "모델별 사용량", + toolsAria: "도구별 사용법", + pricingAria: "사용량 가격 구성", + requestsAria: "사용 활동 로그", + providerHeaders: ["공급자", "통화", "토큰", "비용"], + modelHeaders: ["모델", "통화", "토큰", "비용"], + toolHeaders: ["도구", "통화", "성공", "오류", "평균 지속 시간"], + pricingHeaders: ["공급자", "모델", "입력 / 1M", "출력 / 1M"], + requestHeaders: ["시간", "유형", "목표", "일", "토큰", "비용", "숨어 있음", "상태"], + noPricing: "가격 재정의 없음", + modelKind: "모델", + toolKind: "도구", + unknown: "알려지지 않은", + untitledSession: "제목 없는 세션", + openSession: label => `세션 "${label}" 열기`, + success: "성공", + error: "오류", + aborted: "중단됨", + providerEmptyTitle: "공급자 사용 없음", + providerEmptyBody: "모델 호출 후 공급자 호출 횟수, 토큰 및 비용이 여기에 표시됩니다.", + modelEmptyTitle: "모델 사용 없음", + modelEmptyBody: "모델 호출 후 모델별로 호출 횟수, 토큰, 비용이 여기에 표시됩니다.", + toolEmptyTitle: "도구 호출 없음", + toolEmptyBody: "상담원이 도구를 호출한 후 도구별 통화, 성공, 오류 및 평균 지속 시간이 여기에 표시됩니다.", + pricingEmptyBody: "가격 재정의가 없으면 비용은 기본 제공 모델 가격표를 사용합니다. 특정 모델에 대한 맞춤 가격을 여기에 추가하세요." + } + } } satisfies UiCatalog; - export function getUsageSettingsCopy(locale: UiLocale): UsageSettingsCopy { return SETTINGS_USAGE_COPY[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts index 9f42cbfcc7..42daed9812 100644 --- a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts @@ -18,60 +18,327 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; - import type { WebSearchCredentialStatus, WebSearchErrorReason } from '@maka/core/web-search'; - export type WebSearchSettingsCopy = { - saveFailed: string; saveStatusFailed: string; keySaved: string; keySavedDetail: string; credentialsCleared: string; credentialsClearedDetail: string; - credentialValid: string; resultCount(count: number): string; testFailed: string; testError: string; enabled: string; enabledHelp: string; - provider: string; providerHelp: string; providerModel: string; providerTavily: string; - modelCredential: string; modelCredentialHelp: string; - statusAria: string; lastTest: string; enabledAria: string; key: string; envKeyHelp: string; savedKeyHelp: string; - envPlaceholder: string; storedPlaceholder: string; keyPlaceholder: string; keyAria: string; actions: string; actionsHelp: string; - saving: string; saveKey: string; testing: string; testKey: string; clearing: string; clearKey: string; - testSearch: string; testSearchHelp: string; queryPlaceholder: string; - searching: string; search: string; queryFailed(error: string): string; noResults: string; resultsAria: string; - disabledReasons: { noKey: string; disabled: string; noQuery: string }; - statuses: Record & { validEnabled: string; validDisabled: string; unknownEnabled: string; modelEnabled: string; modelDisabled: string }; - sources: { model: string; envWithSaved: string; env: string; saved: string; none: string }; + saveFailed: string; + saveStatusFailed: string; + keySaved: string; + keySavedDetail: string; + credentialsCleared: string; + credentialsClearedDetail: string; + credentialValid: string; + resultCount(count: number): string; + testFailed: string; + testError: string; + enabled: string; + enabledHelp: string; + provider: string; + providerHelp: string; + providerModel: string; + providerTavily: string; + modelCredential: string; + modelCredentialHelp: string; + statusAria: string; + lastTest: string; + enabledAria: string; + key: string; + envKeyHelp: string; + savedKeyHelp: string; + envPlaceholder: string; + storedPlaceholder: string; + keyPlaceholder: string; + keyAria: string; + actions: string; + actionsHelp: string; + saving: string; + saveKey: string; + testing: string; + testKey: string; + clearing: string; + clearKey: string; + testSearch: string; + testSearchHelp: string; + queryPlaceholder: string; + searching: string; + search: string; + queryFailed(error: string): string; + noResults: string; + resultsAria: string; + disabledReasons: { + noKey: string; + disabled: string; + noQuery: string; + }; + statuses: Record & { + validEnabled: string; + validDisabled: string; + unknownEnabled: string; + modelEnabled: string; + modelDisabled: string; + }; + sources: { + model: string; + envWithSaved: string; + env: string; + saved: string; + none: string; + }; errors: Record; }; - -const SETTINGS_WEB_SEARCH_COPY = { +const SETTINGS_WEB_SEARCH_COPY_BASE = { zh: { - saveFailed: '保存联网搜索设置失败', saveStatusFailed: '保存联网搜索状态失败', keySaved: '已保存 Tavily 密钥', keySavedDetail: '可点击「测试」做一次真实请求验证。', - credentialsCleared: '已清空 Tavily 凭据', credentialsClearedDetail: '联网搜索已自动关闭。', credentialValid: 'Tavily 凭据可用', resultCount: (count) => `返回 ${count} 条结果。`, - testFailed: '联网搜索测试失败', testError: '联网搜索测试出错', enabled: '启用联网搜索', enabledHelp: '启用后,Maka 可以在需要最新外部信息时调用所选搜索来源。', - provider: '搜索来源', providerHelp: '优先复用当前模型的服务端搜索;不支持时可显式改用 Tavily。', providerModel: '当前模型', providerTavily: 'Tavily', - modelCredential: '主模型原生搜索', modelCredentialHelp: 'Maka 会在每个任务回合开始时,根据当前连接与精确模型决定是否把原生 web_search 注入同一次模型请求。不保存第二份搜索密钥,也不会从设置页另发一次模型调用。', - statusAria: '联网搜索凭据状态', lastTest: '最近测试 ', enabledAria: '启用联网搜索', key: 'Tavily 密钥', - envKeyHelp: '当前使用环境变量 TAVILY_API_KEY / MAKA_TAVILY_API_KEY;如需改用保存的密钥,请移除环境变量后重启。', savedKeyHelp: '密钥只保存在本机。申请地址:', - envPlaceholder: '由环境变量提供', storedPlaceholder: '已保存(输入新密钥可替换)', keyPlaceholder: 'tvly-xxxxxxxx', keyAria: 'Tavily 密钥', - actions: '凭据操作', actionsHelp: '保存后可以测试一次真实请求;清空凭据会同步关闭联网搜索。', saving: '保存中…', saveKey: '保存密钥', testing: '测试中…', testKey: '测试凭据', clearing: '清空中…', clearKey: '清空密钥', - testSearch: '测试搜索', testSearchHelp: '发一条真实查询,确认所选联网搜索来源是否配置可用。结果只显示在这里,不写入任务。', queryPlaceholder: '例如:本周 AI 产品发布动态', - searching: '搜索中…', search: '搜索', queryFailed: (error) => `查询失败:${error}`, noResults: '没有结果。', resultsAria: '联网搜索真实查询结果', - disabledReasons: { noKey: '先配置所选搜索来源', disabled: '先启用联网搜索', noQuery: '输入查询后再搜索' }, - statuses: { valid: '已验证', invalid_credentials: '密钥无效', rate_limited: '服务限流', timeout: '测试超时', network_error: '网络异常', not_configured: '等待配置', untested: '未测试', validEnabled: '已验证 · 已启用', validDisabled: '已验证 · 未启用', unknownEnabled: '未测试 · 已启用', modelEnabled: '已启用 · 按任务模型判定', modelDisabled: '当前模型来源 · 未启用' }, - sources: { model: '来源:当前模型连接', envWithSaved: '来源:环境变量(已保存密钥备用)', env: '来源:环境变量', saved: '来源:本机已保存密钥', none: '来源:未配置' }, - errors: { invalid_query: '请输入有效的搜索内容。', incognito_active: '无痕模式下无法使用联网搜索。', not_configured: '所选搜索来源尚未配置完成。', invalid_credentials: '搜索来源拒绝了当前凭据,请更新后重试。', rate_limited: '搜索请求过于频繁,请稍后重试。', network_error: '网络请求失败,请检查网络后重试。', timeout: '搜索请求超时,请重试。', unsupported_provider: '当前模型不支持服务端搜索,或 Maka 尚未实现它的协议;可改用 Tavily。', experimental_disabled: '联网搜索实验功能当前已关闭。' }, + saveFailed: '保存联网搜索设置失败', + saveStatusFailed: '保存联网搜索状态失败', + keySaved: '已保存 Tavily 密钥', + keySavedDetail: '可点击「测试」做一次真实请求验证。', + credentialsCleared: '已清空 Tavily 凭据', + credentialsClearedDetail: '联网搜索已自动关闭。', + credentialValid: 'Tavily 凭据可用', + resultCount: count => `返回 ${count} 条结果。`, + testFailed: '联网搜索测试失败', + testError: '联网搜索测试出错', + enabled: '启用联网搜索', + enabledHelp: '启用后,Maka 可以在需要最新外部信息时调用所选搜索来源。', + provider: '搜索来源', + providerHelp: '优先复用当前模型的服务端搜索;不支持时可显式改用 Tavily。', + providerModel: '当前模型', + providerTavily: 'Tavily', + modelCredential: '主模型原生搜索', + modelCredentialHelp: 'Maka 会在每个任务回合开始时,根据当前连接与精确模型决定是否把原生 web_search 注入同一次模型请求。不保存第二份搜索密钥,也不会从设置页另发一次模型调用。', + statusAria: '联网搜索凭据状态', + lastTest: '最近测试 ', + enabledAria: '启用联网搜索', + key: 'Tavily 密钥', + envKeyHelp: '当前使用环境变量 TAVILY_API_KEY / MAKA_TAVILY_API_KEY;如需改用保存的密钥,请移除环境变量后重启。', + savedKeyHelp: '密钥只保存在本机。申请地址:', + envPlaceholder: '由环境变量提供', + storedPlaceholder: '已保存(输入新密钥可替换)', + keyPlaceholder: 'tvly-xxxxxxxx', + keyAria: 'Tavily 密钥', + actions: '凭据操作', + actionsHelp: '保存后可以测试一次真实请求;清空凭据会同步关闭联网搜索。', + saving: '保存中…', + saveKey: '保存密钥', + testing: '测试中…', + testKey: '测试凭据', + clearing: '清空中…', + clearKey: '清空密钥', + testSearch: '测试搜索', + testSearchHelp: '发一条真实查询,确认所选联网搜索来源是否配置可用。结果只显示在这里,不写入任务。', + queryPlaceholder: '例如:本周 AI 产品发布动态', + searching: '搜索中…', + search: '搜索', + queryFailed: error => `查询失败:${error}`, + noResults: '没有结果。', + resultsAria: '联网搜索真实查询结果', + disabledReasons: { + noKey: '先配置所选搜索来源', + disabled: '先启用联网搜索', + noQuery: '输入查询后再搜索' + }, + statuses: { + valid: '已验证', + invalid_credentials: '密钥无效', + rate_limited: '服务限流', + timeout: '测试超时', + network_error: '网络异常', + not_configured: '等待配置', + untested: '未测试', + validEnabled: '已验证 · 已启用', + validDisabled: '已验证 · 未启用', + unknownEnabled: '未测试 · 已启用', + modelEnabled: '已启用 · 按任务模型判定', + modelDisabled: '当前模型来源 · 未启用' + }, + sources: { + model: '来源:当前模型连接', + envWithSaved: '来源:环境变量(已保存密钥备用)', + env: '来源:环境变量', + saved: '来源:本机已保存密钥', + none: '来源:未配置' + }, + errors: { + invalid_query: '请输入有效的搜索内容。', + incognito_active: '无痕模式下无法使用联网搜索。', + not_configured: '所选搜索来源尚未配置完成。', + invalid_credentials: '搜索来源拒绝了当前凭据,请更新后重试。', + rate_limited: '搜索请求过于频繁,请稍后重试。', + network_error: '网络请求失败,请检查网络后重试。', + timeout: '搜索请求超时,请重试。', + unsupported_provider: '当前模型不支持服务端搜索,或 Maka 尚未实现它的协议;可改用 Tavily。', + experimental_disabled: '联网搜索实验功能当前已关闭。' + } }, en: { - saveFailed: 'Failed to save web search settings', saveStatusFailed: 'Failed to save web search status', keySaved: 'Tavily key saved', keySavedDetail: 'Select Test credentials to verify it with a real request.', - credentialsCleared: 'Tavily credentials cleared', credentialsClearedDetail: 'Web search was disabled automatically.', credentialValid: 'Tavily credentials work', resultCount: (count) => `Returned ${count} ${count === 1 ? 'result' : 'results'}.`, - testFailed: 'Web search test failed', testError: 'Web search test error', enabled: 'Enable web search', enabledHelp: 'When enabled, Maka can call the selected search source for current external information.', - provider: 'Search source', providerHelp: 'Reuse the current model provider when it supports hosted search, or explicitly use Tavily.', providerModel: 'Current model', providerTavily: 'Tavily', - modelCredential: 'Primary-model native search', modelCredentialHelp: 'At the start of each turn, Maka uses the current connection and exact model to decide whether to inject native web_search into the same model request. It stores no second search key and sends no separate model call from Settings.', - statusAria: 'Web search credential status', lastTest: 'Last tested ', enabledAria: 'Enable web search', key: 'Tavily key', - envKeyHelp: 'Currently using TAVILY_API_KEY / MAKA_TAVILY_API_KEY from the environment. Remove the environment variable and restart to use a saved key.', savedKeyHelp: 'The key is stored only on this machine. Apply at: ', - envPlaceholder: 'Provided by environment variable', storedPlaceholder: 'Saved (enter a new key to replace)', keyPlaceholder: 'tvly-xxxxxxxx', keyAria: 'Tavily key', - actions: 'Credential actions', actionsHelp: 'After saving, test with a real request. Clearing credentials also disables web search.', saving: 'Saving…', saveKey: 'Save key', testing: 'Testing…', testKey: 'Test credentials', clearing: 'Clearing…', clearKey: 'Clear key', - testSearch: 'Test search', testSearchHelp: 'Send a real query to confirm the selected web search source is configured and working. Results appear here only and are not written to the task.', queryPlaceholder: 'For example: AI product launches this week', - searching: 'Searching…', search: 'Search', queryFailed: (error) => `Query failed: ${error}`, noResults: 'No results.', resultsAria: 'Web search live query results', - disabledReasons: { noKey: 'Configure the selected search source first', disabled: 'Enable web search first', noQuery: 'Enter a query before searching' }, - statuses: { valid: 'Verified', invalid_credentials: 'Invalid key', rate_limited: 'Rate limited', timeout: 'Test timed out', network_error: 'Network error', not_configured: 'Needs setup', untested: 'Not tested', validEnabled: 'Verified · enabled', validDisabled: 'Verified · disabled', unknownEnabled: 'Not tested · enabled', modelEnabled: 'Enabled · checked per task model', modelDisabled: 'Current model source · disabled' }, - sources: { model: 'Source: current model connection', envWithSaved: 'Source: environment variable (saved key available as backup)', env: 'Source: environment variable', saved: 'Source: key saved on this device', none: 'Source: not configured' }, - errors: { invalid_query: 'Enter a valid search query.', incognito_active: 'Web search is unavailable in incognito mode.', not_configured: 'The selected search source is not configured.', invalid_credentials: 'The search provider rejected the current credential. Update it and try again.', rate_limited: 'The search provider is receiving too many requests. Try again later.', network_error: 'The network request failed. Check your connection and try again.', timeout: 'The search request timed out. Try again.', unsupported_provider: 'The current model does not support hosted search, or Maka has not implemented its protocol yet. Select Tavily to continue.', experimental_disabled: 'The experimental web search feature is currently disabled.' }, - }, + saveFailed: 'Failed to save web search settings', + saveStatusFailed: 'Failed to save web search status', + keySaved: 'Tavily key saved', + keySavedDetail: 'Select Test credentials to verify it with a real request.', + credentialsCleared: 'Tavily credentials cleared', + credentialsClearedDetail: 'Web search was disabled automatically.', + credentialValid: 'Tavily credentials work', + resultCount: count => `Returned ${count} ${count === 1 ? 'result' : 'results'}.`, + testFailed: 'Web search test failed', + testError: 'Web search test error', + enabled: 'Enable web search', + enabledHelp: 'When enabled, Maka can call the selected search source for current external information.', + provider: 'Search source', + providerHelp: 'Reuse the current model provider when it supports hosted search, or explicitly use Tavily.', + providerModel: 'Current model', + providerTavily: 'Tavily', + modelCredential: 'Primary-model native search', + modelCredentialHelp: 'At the start of each turn, Maka uses the current connection and exact model to decide whether to inject native web_search into the same model request. It stores no second search key and sends no separate model call from Settings.', + statusAria: 'Web search credential status', + lastTest: 'Last tested ', + enabledAria: 'Enable web search', + key: 'Tavily key', + envKeyHelp: 'Currently using TAVILY_API_KEY / MAKA_TAVILY_API_KEY from the environment. Remove the environment variable and restart to use a saved key.', + savedKeyHelp: 'The key is stored only on this machine. Apply at: ', + envPlaceholder: 'Provided by environment variable', + storedPlaceholder: 'Saved (enter a new key to replace)', + keyPlaceholder: 'tvly-xxxxxxxx', + keyAria: 'Tavily key', + actions: 'Credential actions', + actionsHelp: 'After saving, test with a real request. Clearing credentials also disables web search.', + saving: 'Saving…', + saveKey: 'Save key', + testing: 'Testing…', + testKey: 'Test credentials', + clearing: 'Clearing…', + clearKey: 'Clear key', + testSearch: 'Test search', + testSearchHelp: 'Send a real query to confirm the selected web search source is configured and working. Results appear here only and are not written to the task.', + queryPlaceholder: 'For example: AI product launches this week', + searching: 'Searching…', + search: 'Search', + queryFailed: error => `Query failed: ${error}`, + noResults: 'No results.', + resultsAria: 'Web search live query results', + disabledReasons: { + noKey: 'Configure the selected search source first', + disabled: 'Enable web search first', + noQuery: 'Enter a query before searching' + }, + statuses: { + valid: 'Verified', + invalid_credentials: 'Invalid key', + rate_limited: 'Rate limited', + timeout: 'Test timed out', + network_error: 'Network error', + not_configured: 'Needs setup', + untested: 'Not tested', + validEnabled: 'Verified · enabled', + validDisabled: 'Verified · disabled', + unknownEnabled: 'Not tested · enabled', + modelEnabled: 'Enabled · checked per task model', + modelDisabled: 'Current model source · disabled' + }, + sources: { + model: 'Source: current model connection', + envWithSaved: 'Source: environment variable (saved key available as backup)', + env: 'Source: environment variable', + saved: 'Source: key saved on this device', + none: 'Source: not configured' + }, + errors: { + invalid_query: 'Enter a valid search query.', + incognito_active: 'Web search is unavailable in incognito mode.', + not_configured: 'The selected search source is not configured.', + invalid_credentials: 'The search provider rejected the current credential. Update it and try again.', + rate_limited: 'The search provider is receiving too many requests. Try again later.', + network_error: 'The network request failed. Check your connection and try again.', + timeout: 'The search request timed out. Try again.', + unsupported_provider: 'The current model does not support hosted search, or Maka has not implemented its protocol yet. Select Tavily to continue.', + experimental_disabled: 'The experimental web search feature is currently disabled.' + } + } +} satisfies Omit, 'ko'>; +const SETTINGS_WEB_SEARCH_COPY = { + ...SETTINGS_WEB_SEARCH_COPY_BASE, + ko: { + saveFailed: "웹 검색 설정을 저장하지 못했습니다.", + saveStatusFailed: "웹 검색 상태를 저장하지 못했습니다.", + keySaved: "Tavily 키가 저장되었습니다.", + keySavedDetail: "실제 요청으로 검증하려면 자격 증명 테스트를 선택하세요.", + credentialsCleared: "Tavily 자격 증명이 삭제되었습니다.", + credentialsClearedDetail: "웹 검색이 자동으로 비활성화되었습니다.", + credentialValid: "Tavily 자격 증명 작업", + resultCount: count => `${count} ${count === 1 ? 'result' : 'results'}을(를) 반환했습니다.`, + testFailed: "웹 검색 테스트 실패", + testError: "웹 검색 테스트 오류", + enabled: "웹 검색 활성화", + enabledHelp: "활성화되면 Maka는 현재 외부 정보에 대해 선택한 검색 소스를 호출할 수 있습니다.", + provider: "소스 검색", + providerHelp: "호스팅된 검색을 지원하는 경우 현재 모델 공급자를 재사용하거나 Tavily를 명시적으로 사용하세요.", + providerModel: "현재 모델", + providerTavily: "타빌리", + modelCredential: "기본 모델 기본 검색", + modelCredentialHelp: "각 턴이 시작될 때 Maka는 현재 연결과 정확한 모델을 사용하여 기본 web_search를 동일한 모델 요청에 삽입할지 여부를 결정합니다. 두 번째 검색 키를 저장하지 않으며 설정에서 별도의 모델 호출을 보내지 않습니다.", + statusAria: "웹 검색 자격 증명 상태", + lastTest: "마지막 테스트", + enabledAria: "웹 검색 활성화", + key: "타빌리 키", + envKeyHelp: "현재 환경에서 TAVILY_API_KEY / MAKA_TAVILY_API_KEY를 사용하고 있습니다. 저장된 키를 사용하려면 환경 변수를 제거하고 다시 시작하세요.", + savedKeyHelp: "키는 이 시스템에만 저장됩니다. 신청 장소:", + envPlaceholder: "환경 변수에서 제공", + storedPlaceholder: "저장됨(교체할 새 키 입력)", + keyPlaceholder: 'tvly-xxxxxxxx', + keyAria: "타빌리 키", + actions: "자격 증명 작업", + actionsHelp: "저장 후 실제 요청으로 테스트해 보세요. 자격 증명을 지우면 웹 검색도 비활성화됩니다.", + saving: "절약…", + saveKey: "키 저장", + testing: "테스트 중…", + testKey: "테스트 자격 증명", + clearing: "청산…", + clearKey: "클리어 키", + testSearch: "테스트 검색", + testSearchHelp: "선택한 웹 검색 소스가 구성되어 작동하는지 확인하기 위해 실제 쿼리를 보냅니다. 결과는 여기에만 표시되며 작업에는 기록되지 않습니다.", + queryPlaceholder: "예: 이번 주에 AI 제품 출시", + searching: "수색…", + search: "찾다", + queryFailed: error => `쿼리 실패: ${error}`, + noResults: "결과가 없습니다.", + resultsAria: "웹 검색 실시간 쿼리 결과", + disabledReasons: { + noKey: "선택한 검색 소스를 먼저 구성하세요.", + disabled: "먼저 웹 검색을 활성화하세요.", + noQuery: "검색하기 전에 검색어를 입력하세요" + }, + statuses: { + valid: "확인됨", + invalid_credentials: "잘못된 키", + rate_limited: "요금 제한", + timeout: "테스트 시간이 초과되었습니다.", + network_error: "네트워크 오류", + not_configured: "설정 필요", + untested: "테스트되지 않음", + validEnabled: "확인됨 · 활성화됨", + validDisabled: "확인됨 · 사용 중지됨", + unknownEnabled: "테스트되지 않음 · 활성화됨", + modelEnabled: "활성화됨 · 작업 모델별로 확인됨", + modelDisabled: "현재 모델 소스 · 비활성화됨" + }, + sources: { + model: "출처: 현재 모델 연결", + envWithSaved: "출처: 환경 변수(저장된 키는 백업으로 사용 가능)", + env: "출처: 환경변수", + saved: "소스: 이 기기에 저장된 키", + none: "소스: 구성되지 않음" + }, + errors: { + invalid_query: "유효한 검색어를 입력하세요.", + incognito_active: "시크릿 모드에서는 웹 검색을 사용할 수 없습니다.", + not_configured: "선택한 검색 소스가 구성되지 않았습니다.", + invalid_credentials: "검색 공급자가 현재 자격 증명을 거부했습니다. 업데이트하고 다시 시도하세요.", + rate_limited: "검색 공급자가 너무 많은 요청을 받고 있습니다. 나중에 다시 시도하세요.", + network_error: "네트워크 요청이 실패했습니다. 연결을 확인하고 다시 시도하세요.", + timeout: "검색 요청 시간이 초과되었습니다. 다시 시도해 보세요.", + unsupported_provider: "현재 모델은 호스팅 검색을 지원하지 않거나 Maka가 아직 해당 프로토콜을 구현하지 않았습니다. 계속하려면 Tavily를 선택하세요.", + experimental_disabled: "실험적인 웹 검색 기능은 현재 비활성화되어 있습니다." + } + } } satisfies UiCatalog; - -export function getWebSearchSettingsCopy(locale: UiLocale): WebSearchSettingsCopy { return SETTINGS_WEB_SEARCH_COPY[locale]; } +export function getWebSearchSettingsCopy(locale: UiLocale): WebSearchSettingsCopy { + return SETTINGS_WEB_SEARCH_COPY[locale]; +} diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts index ed545587d2..1ced874def 100644 --- a/apps/desktop/src/renderer/locales/shell-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-copy.ts @@ -1811,6 +1811,557 @@ const SHELL_COPY_BY_LOCALE = { resizeWorkbar: 'Resize task workbar', }, }, + ko: { + navigation: { settings: 'Settings' }, + actions: { retry: 'Retry' }, + paths: { + workspace: 'workspace', + project: 'project folder', + skills: 'Skills folder', + }, + errors: { + messageRead: 'Task content is temporarily unavailable. Try again later.', + messageRefresh: 'Task content could not be refreshed. Try again later.', + openPath: (path: string) => `Could not open the ${path}. Try again later.`, + workspaceUnavailableTitle: 'Working directory unavailable', + workspaceUnavailableDescription: + 'The working directory does not exist or cannot be accessed. Select a valid folder for a new task.', + }, + chatActions: { + newConversation: 'New task', + sendFailedTitle: 'Message not sent', + sendFailedFallback: 'The message could not be sent. Try again later.', + skillInvocationBlockedTitle: 'Skill invocation failed; message not sent', + skillInvocationBlockedDescription: (items) => `${items.join(', ')}. Adjust the selection and try again.`, + skillInvocationFailedTitle: 'Some Skills were not invoked', + skillInvocationFailedDescription: (items) => + `${items.join(', ')}. The remaining Skills were invoked.`, + skillInvocationFailureReason: { + invalid_name: 'invalid name', + not_found: 'not found', + disabled: 'disabled', + host_incompatible: 'required tools unavailable', + resolution_failed: 'resolution failed', + too_many_requests: 'more than 50 distinct Skill invocation requests', + }, + responseFailedTitle: 'Response failed', + responseFailedFallback: 'The task action failed. Try again later.', + refreshFailedTitle: 'Could not refresh task', + sessionStartFailedTitle: 'Could not start task', + sessionStartFailedFallback: 'The task could not be started. Try again later.', + }, + projectActions: { + currentProject: 'Current project', + readPathFailedTitle: 'Could not read project path', + readPathFailedFallback: 'The project path is temporarily unavailable. Try again later.', + selectDirectoryFailedTitle: 'Could not select working directory', + selectedPathUnreadable: 'The selected path does not exist or cannot be read.', + directorySwitchedTitle: 'Working directory changed', + projectUpdateFailedTitle: 'Could not update project', + projectUpdateFailedFallback: 'The project could not be updated. Try again later.', + catalogUnavailable: 'Runtime Hosts unavailable', + retryCatalog: 'Retry loading', + remoteDirectoryTitle: (host: string) => `Add a project on ${host}`, + remoteDirectoryBreadcrumbs: 'Current folder', + remoteDirectoryHome: 'Home', + remoteDirectoryEmpty: 'No folders here', + remoteDirectorySelect: 'Add this folder', + remoteDirectoryCancel: 'Cancel', + remoteDirectoryRetry: 'Retry', + remoteDirectoryLoading: 'Loading folders…', + remoteDirectoryShowHidden: 'Show hidden folders', + remoteDirectoryHideHidden: 'Hide hidden folders', + runtimeHostReadiness: { + connecting: 'Connecting', + reconnecting: 'Reconnecting', + unavailable: 'Unavailable', + }, + openFailedTitle: (path: string) => `Could not open ${path}`, + openPathLabels: { + workspace: 'workspace folder', + skills: 'Skills folder', + memory: 'memory folder', + project: 'project folder', + }, + openPathFailures: { + 'unknown-key': 'Unknown workspace folder.', + 'not-allowed': 'The path is outside the folders that Maka can open.', + missing: 'The folder does not exist.', + 'not-a-directory': 'The target is not a folder.', + 'open-failed': 'The system could not open the folder.', + unknown: 'The folder could not be opened.', + }, + }, + commandActions: { + connectionVerified: (name: string) => `Connection verified · ${name}`, + connectionLatency: (latency: number | string, model?: string) => + `Latency ${latency} ms${model ? ` · ${model}` : ''}`, + connectionTestFailed: (name: string) => `Connection test failed · ${name}`, + testErrorTitle: 'Test error', + connectionUnavailable: 'Connection testing is temporarily unavailable. Try again later.', + connectionFailures: { + rateLimit: 'The account or model service is rate limited. Try again later.', + timeout: 'The request timed out. Check the network or proxy and try again.', + auth: 'Authentication failed. Check the model key, subscription login, or credentials and try again.', + network: 'Network error. Check the network or proxy and try again.', + provider: 'The model service returned an error. Try again later.', + unknown: 'The connection test failed. Try again later.', + }, + setDefaultSuccess: (name: string) => `Set as default · ${name}`, + setDefaultFailedTitle: 'Could not change default', + setDefaultFallback: 'The default model could not be changed. Try again later.', + newConversation: 'New task', + conversationCopiedTitle: 'Task copied as Markdown', + lineCount: (lines: number) => `${lines} lines · Ready for Notion / Obsidian / GitHub`, + copyFailedTitle: 'Copy failed', + clipboardUnavailable: 'Clipboard unavailable', + conversationSavedTitle: 'Task saved', + saveSummary: (lines: number, fileName: string) => `${lines} lines · Saved as ${fileName}`, + saveFailedTitle: 'Save failed', + invalidExport: 'The export content is invalid', + writeFailed: 'The selected location could not be written', + exportFallback: 'The task could not be exported. Try again later.', + memoryOpenFailedTitle: 'Could not open MEMORY.md', + openFailedTitle: 'Open failed', + memoryOpenFallback: 'MEMORY.md could not be opened. Try again later.', + today: 'Today', + reviewCopiedTitle: "Today's review copied as Markdown", + reviewSummary: (sessions: number, requests: number) => `${sessions} tasks · ${requests} requests`, + reviewCopyFallback: "Today's review is unavailable, or the clipboard was denied.", + reviewPastedTitle: "Today's review added to the composer", + reviewCopied: (label: string) => `${label} review copied`, + reviewPasted: (label: string) => `${label} review added to the composer`, + reviewSaved: (label: string) => `${label} review saved`, + reviewSaveFallback: 'The Daily Review could not be saved. Try again later.', + pasteFailedTitle: 'Paste failed', + reviewUnavailable: "Today's review is temporarily unavailable. Try again later.", + diagnosticsCopiedTitle: 'Diagnostics copied', + diagnosticsCopiedDescription: 'Review the contents, then paste them into the issue report', + clipboardDenied: 'The clipboard is unavailable or was denied', + networkPassedTitle: 'Network proxy test passed', + networkFailedTitle: 'Network proxy test failed', + genericTestFailedTitle: 'Test failed', + networkTestFallback: 'Network proxy testing is temporarily unavailable. Try again later.', + }, + sessionRowActions: { + actionFallback: 'The task action failed. Try again later.', + flagFailedTitle: 'Could not flag task', + unflagFailedTitle: 'Could not remove flag', + archiveFailedTitle: 'Could not archive task', + unarchiveFailedTitle: 'Could not restore task', + renameFailedTitle: 'Could not rename task', + deleteFailedTitle: 'Could not delete task', + currentConversation: 'Current task', + deleteTitle: (name: string) => `Delete "${name}"`, + deleteDescription: + 'The task and all of its messages will be permanently removed from disk. This cannot be undone.', + deleteLabel: 'Delete', + cancelLabel: 'Cancel', + deletedTitle: (name: string) => `Deleted ${name}`, + deleteRestoredTitle: (name: string) => `${name} was restored, so it was kept`, + deleteSubtaskNote: () => 'Its ordinary subtasks will be kept and moved to Archived.', + deleteSubtaskNoteUncertain: () => + 'Its ordinary subtasks, if any, will be kept and moved to Archived.', + deletedSubtaskNote: (count: number) => + count === 1 ? '1 subtask moved to Archived' : `${count} subtasks moved to Archived`, + bulkArchiveDescription: 'Archived tasks stay available under Settings › Activity.', + bulkArchivedTitle: (count: number) => `Archived ${count} tasks`, + bulkArchiveFailedTitle: 'Some tasks were not archived', + bulkFailedBody: (count: number) => `${count} of them did not go through.`, + }, + skillActions: { + refreshSkillsFailedTitle: 'Could not refresh Skills', + refreshSkillsFallback: 'Skills could not be refreshed. Try again later.', + refreshSourcesFailedTitle: 'Could not refresh Skill sources', + refreshSourcesFallback: 'Skill sources could not be refreshed. Try again later.', + refreshBundledFailedTitle: 'Could not refresh built-in Skills', + refreshBundledFallback: 'Built-in Skills could not be refreshed. Try again later.', + installBundledFailedTitle: 'Could not install built-in Skill', + installBundledFallback: 'The built-in Skill could not be installed. Try again later.', + installedBundledTitle: 'Built-in Skill installed', + installedDescription: (id: string) => `${id}/SKILL.md was added to the current workspace.`, + importSourceFailedTitle: 'Could not import Skill source', + importSourceFallback: 'The Skill source could not be imported. Try again later.', + importedSourceTitle: 'Skill source imported', + installFailedTitle: 'Could not install Skill', + installFallback: 'The Skill could not be installed. Try again later.', + installedTitle: 'Skill installed', + previewFailedTitle: 'Could not preview Skill update', + previewFallback: 'The Skill update could not be previewed. Try again later.', + updateFailedTitle: 'Could not update Skill', + updateFallback: 'The Skill could not be updated. Try again later.', + updatedTitle: 'Skill updated', + forceUpdatedTitle: 'Skill update overwritten', + updatedDescription: (id: string) => `${id}/SKILL.md was updated to the source-library version.`, + toggleFailedTitle: 'Could not change Skill status', + toggleFallback: 'The Skill status could not be changed. Try again later.', + enabledTitle: 'Skill enabled', + disabledTitle: 'Skill disabled', + pinnedTitle: 'Skill pinned to context', + unpinnedTitle: 'Skill unpinned', + runtimeDescription: (name: string) => `${name} runtime status was updated for the current project.`, + deleteFailedTitle: 'Could not delete Skill', + deleteFallback: 'The Skill could not be deleted. Try again later.', + deletedTitle: 'Skill deleted', + deletedDescription: (id: string) => `${id} was removed.`, + openFailedTitle: 'Could not open Skill', + openFallback: 'The Skill could not be opened. Try again later.', + openFailures: { + invalid_id: 'The Skill name is not allowed.', + missing: 'The matching SKILL.md was not found.', + blocked_path: 'The Skill path is outside the workspace skills folder, so opening was blocked.', + not_file: 'The target is not an openable SKILL.md file.', + not_directory: 'The target is not an openable folder.', + open_failed: 'The system could not open the file.', + }, + sourceFailures: { + invalid_skill: 'Select a valid SKILL.md file.', + already_exists: 'A Skill with the same name already exists in the source library.', + blocked_path: 'This file path cannot be imported.', + write_failed: 'The source library could not be written. Check file permissions.', + cancelled: 'Cancelled.', + }, + installFailures: { + not_found: 'This Skill source was not found.', + already_exists: 'A Skill with the same name already exists in this workspace.', + blocked_path: 'The target path cannot be written.', + write_failed: 'The workspace could not be written. Check file permissions.', + }, + updateFailures: { + not_managed: 'This Skill is not from a managed source.', + source_missing: 'The matching source was not found in the source library.', + local_modified: + 'The workspace copy was modified. Open the local and source files to compare them before updating.', + metadata_error: 'The Skill metadata is invalid, so it cannot be updated safely.', + blocked_path: 'The target path cannot be written.', + write_failed: 'The workspace could not be written. Check file permissions.', + }, + previewFailures: { + not_managed: 'This Skill is not from a managed source.', + source_missing: 'The matching source was not found in the source library.', + metadata_error: 'The Skill metadata is invalid, so it cannot be previewed safely.', + blocked_path: 'The target path cannot be read.', + read_failed: 'The Skill content could not be read. Check file permissions.', + }, + deleteFailures: { + not_found: 'This Skill was not found in the current workspace.', + blocked_path: 'The Skill path cannot be deleted.', + blocked_scope: 'Project Skills are managed by the repository. Delete it from the project instead.', + delete_failed: 'The Skill could not be deleted. Check file permissions.', + }, + runtimeFailures: { + not_found: 'This Skill was not found in the current workspace.', + blocked_path: 'The Skill status path cannot be written.', + state_error: 'The Skill status file in this workspace is invalid and must be fixed first.', + write_failed: 'The Skill status for the current project could not be written. Check file permissions.', + }, + }, + sessionSettingsActions: { + bypassConfirmTitle: 'Switch to full access?', + bypassConfirmDescription: + "Local tools will read and write your files and reach the network directly, outside Maka's protection layer. Use only for tasks you fully trust, or ones already isolated by their environment.", + bypassConfirmLabel: 'Turn on full access', + bypassCancelLabel: 'Keep Auto', + permissionFailedTitle: 'Could not change permission mode', + permissionFallback: 'The permission mode could not be changed. Try again later.', + modelFailedTitle: 'Could not change model', + modelFallback: 'The model could not be changed. Try again later.', + thinkingFailedTitle: 'Could not change thinking level', + thinkingFallback: 'The thinking level could not be changed. Try again later.', + }, + goalDialog: { + title: 'Set a goal', + description: 'Maka continues on its own after each turn until the goal is met, judged impossible, or a budget below is reached. You can stop it any time from above the composer.', + conditionLabel: 'Completion condition', + conditionDescription: 'One sentence for what counts as done; Maka checks it after every turn.', + conditionPlaceholder: 'e.g. all tests pass and lint reports no warnings', + maxIterationsLabel: 'Maximum turns', + maxIterationsDescription: 'Leave empty to use the default.', + maxIterationsInvalid: (max) => `Enter a whole number from 1 to ${max}, or leave it empty.`, + tokenBudgetLabel: 'Token budget', + tokenBudgetDescription: 'Leave empty for no token ceiling.', + tokenBudgetInvalid: (min) => `Enter a whole number of at least ${min}, or leave it empty.`, + cancel: 'Cancel', + close: 'Close', + submit: 'Start', + failedFallback: 'The goal could not be set. Try again.', + statusLabels: { + active: 'Active', + waiting: 'Waiting', + paused: 'Paused', + achieved: 'Achieved', + impossible: 'Impossible', + cleared: 'Cleared', + stalled: 'Stalled', + budget_limited: 'Token budget reached', + max_iterations: 'Maximum turns reached', + }, + reconciledMatching: (condition, status) => + `Current Goal after reconnect: “${condition}” (${status}). It matches your request, but this does not prove that the interrupted operation committed.`, + reconciledDifferent: (condition, status) => + `Current Goal after reconnect: “${condition}” (${status}). It differs from this request.`, + reconciledNoGoal: 'Current state after reconnect: no Goal was found.', + reconciliationUnavailable: 'The connection was interrupted and the current Goal cannot be confirmed yet. Close and reopen to check again; this dialog will not submit twice.', + }, + errorBoundary: { + copyPending: 'Copying…', + copied: 'Copied', + copyFailed: 'Copy failed', + copyReport: 'Copy diagnostics', + title: 'The Maka renderer crashed', + description: + 'An unhandled React error was caught. Try again to clear this crash, or reload to refresh the entire window. Copy the diagnostics before handing off the issue.', + retry: 'Try again', + reload: 'Reload', + clipboardFailure: 'The clipboard is unavailable or was denied. Try again later.', + }, + commandPalette: { + label: 'Command palette', + searchLabel: 'Search the command palette', + placeholder: 'Search commands, settings, or tasks…', + closeLabel: 'Close command palette', + resultsLabel: 'Command palette results', + emptyTitle: 'No matching commands', + emptyDescription: 'Try another search, or press Esc to close.', + selectHint: 'Select', + runHint: 'Run', + closeHint: 'Close', + current: 'Current', + groups: { + settings: 'Settings', + permissions: 'Permissions', + connections: 'Connections', + conversations: 'Tasks', + }, + staticKeywords: STATIC_COMMAND_KEYWORDS, + commands: EN_STATIC_COMMANDS, + settingsSections: EN_SETTINGS_SECTIONS, + permissionModes: { + explore: { + label: 'Permissions · Read only', + hint: 'Read and search directly; confirm writes and network access', + }, + ask: { + label: 'Permissions · Auto', + hint: "Run inside Maka's protection layer; ask before going beyond the current permissions", + }, + bypass: { + label: 'Permissions · Full access', + hint: "Reach your files and your network directly, outside Maka's protection layer", + }, + }, + settingsCommand: (section: string) => `Settings · ${section}`, + testDefaultConnection: (name: string) => `Test default connection · ${name}`, + setDefaultConnection: (name: string) => `Set as default · ${name}`, + testConnection: (name: string) => `Test connection · ${name}`, + settingsKeywords: (section: SettingsSection, label: string) => [section, label, 'settings', '设置'], + permissionKeywords: (mode: PermissionMode) => [mode, 'permission', 'mode', '权限', '模式'], + connectionKeywords: (action: 'default' | 'test', name: string, providerType: string) => [ + action, + 'connection', + '连接', + '默认', + '测试', + name, + providerType, + ], + }, + keyboardHelp: { + title: 'Keyboard shortcuts', + sections: [ + { + heading: 'General', + rows: [ + { + keys: ['⌘', 'K'], + description: 'Open the command palette (tasks, Settings, themes, and more)', + }, + { keys: ['?'], description: 'Open or close this shortcuts panel' }, + { keys: ['⌘', 'N'], description: 'Create a new task' }, + { keys: ['⌘', ','], description: 'Open Settings' }, + { + keys: ['⌘', 'Shift', 'D'], + description: 'Copy diagnostics for the current context', + }, + { keys: ['Esc'], description: 'Close the current dialog' }, + ], + }, + { + heading: 'Composer', + rows: [ + { keys: ['Enter'], description: 'Send the message' }, + { keys: ['Shift', 'Enter'], description: 'Insert a line break' }, + { + keys: ['Alt', 'Enter'], + description: 'Insert a line break (alternative)', + }, + ], + }, + { + heading: 'Task list', + rows: [ + { + keys: ['Tab'], + description: 'Move focus between tasks and navigation', + }, + { + keys: ['↑', '↓'], + description: 'Move through focused tasks', + }, + { + keys: ['Home', 'End'], + description: 'Jump to the top or bottom of the list', + }, + { keys: ['Enter'], description: 'Open the focused task' }, + { + keys: ['Delete'], + description: 'Open the delete confirmation (never delete silently)', + }, + { + keys: ['F'], + description: 'Focus task search (press Esc to clear)', + }, + ], + }, + { + heading: 'Chat', + rows: [ + { + keys: ['Tab'], + description: 'Focus tool activity and Copy buttons', + }, + { + keys: ['Space', 'Enter'], + description: 'Expand or collapse a tool call', + }, + ], + }, + { + heading: 'Panel sizing', + rows: [ + { keys: ['Tab'], description: 'Focus the left or right splitter' }, + { + keys: ['←', '→'], + description: 'Adjust task-list width (±10 px)', + }, + { + keys: ['Shift', '←', '→'], + description: 'Adjust quickly (±50 px)', + }, + { + keys: ['Home', 'End'], + description: 'Jump directly to minimum or maximum width', + }, + ], + }, + ], + }, + chrome: { + windowActions: 'Window shortcuts', + searchConversations: 'Search tasks', + expandSidebar: 'Expand sidebar', + collapseSidebar: 'Collapse sidebar', + newTask: 'New task', + expandWorkbar: 'Expand task workbar', + collapseWorkbar: 'Collapse task workbar', + workspaceActions: 'Workspace actions', + }, + app: { + loadingWorkbarLabel: 'Loading task workbar', + loadingWorkbar: 'Loading task workbar…', + useSkillPrompt: (skillName: string) => `Use the ${skillName} skill: `, + newConversation: 'New task', + compactSuccessTitle: 'Context compacted', + compactSuccessDescription: 'Older context was replaced with a checkpoint summary.', + compactStartedTitle: 'Compacting context', + compactStartedDescription: 'Summarizing older context into a checkpoint.', + compactUnchangedTitle: 'Nothing to compact', + compactUnchangedDescription: 'The task already uses the latest checkpoint.', + compactErrorTitle: 'Compaction failed', + compactErrorFallback: 'The task could not be compacted. Try again later.', + slashCommands: { + compact: { name: 'Compact context', description: 'Compact older history while preserving the current task' }, + graph: { name: 'Use Graph', description: 'Inspect, switch, or run Graph once' }, + side: { name: 'Open side chat', description: 'Start a specific topic in the side panel' }, + swarm: { name: 'Use Swarm', description: 'Inspect, switch, or run Swarm once' }, + }, + sideChatUnavailableTitle: 'Side chat is not available yet', + sideChatUnavailableDescription: + 'Send a message in the main task before using /side.', + sideChatContextPendingTitle: 'Resolve pending context first', + sideChatContextPendingDescription: + 'The Composer still has attachments, quotes, or file mentions. Send or remove them before using /side.', + resumeStartedTitle: 'Continuing this turn', + resumeStartedDescription: 'Continuing from the last complete execution boundary', + resumeFailedTitle: 'Could not continue', + resumeFailedFallback: 'This turn could not be continued. Check the task state and try again.', + goalClearFailedTitle: 'Could not stop the goal', + goalClearFailedFallback: 'The goal may still be running. Try again now.', + goalPauseFailedTitle: 'Could not pause the goal', + goalPauseFailedFallback: 'The goal may still be continuing. Try again now.', + goalResumeFailedTitle: 'Could not resume the goal', + goalResumeFailedFallback: 'The goal is still paused. Try again.', + appearanceLoadErrorTitle: 'Could not load appearance settings', + appearanceLoadErrorFallback: 'Appearance settings are temporarily unavailable. Try again later.', + memoryRefreshErrorTitle: 'Could not refresh local memory status', + memoryLoadErrorTitle: 'Could not load local memory status', + memoryErrorFallback: 'Local memory status could not be refreshed. Try again later.', + openModelSettings: 'Open Settings · Models', + configureModelsOnHost: (hostName: string) => + `Configure a model connection on ${hostName} before starting a task.`, + sidebarCollapsed: 'Sidebar is collapsed', + resizeConversationList: 'Resize task list', + skipErrorTitle: 'Could not skip onboarding', + tryAgainLater: 'Try again later.', + updateInstallFailedTitle: 'Could not install update', + updateInstallFailedFallback: 'Try again later.', + updateInstallManualFallback: 'Try again later, or download the latest version manually.', + updateActiveTasksTitle: 'Tasks are still running', + updateActiveTasksDescription: 'Tasks are still running. Updating will interrupt them. Continue?', + updateActiveTasksConfirm: 'Update anyway', + updateActiveTasksCancel: 'Cancel', + updateRetryFailedTitle: 'Could not retry update download', + updateRetryFailedFallback: 'Try again later, or download the latest version manually.', + loading: 'Loading', + goToModels: 'Go to Models', + boundaryUnreadableTitle: 'Could not read this task’s permissions', + boundaryUnreadableDetail: + 'Until they can be read, you cannot type here. Try again, or switch to another task.', + boundaryUnreadableRetry: 'Try again', + boundaryUnreadableRetrying: 'Trying again…', + permissionModeStreaming: + 'This task is streaming. Wait for it to finish before changing the permission mode.', + permissionModeRunning: 'This task is running. Wait for it to finish before changing the permission mode.', + permissionModeWaiting: 'A tool call is waiting for confirmation. Respond before changing the permission mode.', + modeChangeLoading: 'This session is still loading. Its mode can be changed in a moment.', + modeChanging: 'The mode is changing. Wait for it to finish before continuing.', + modeChangeStreaming: 'This task is streaming. Wait for it to finish before changing the mode.', + modeChangeRunning: 'This task is running. Wait for it to finish before changing the mode.', + modeChangeWaiting: 'A tool call is waiting for confirmation. Respond before changing the mode.', + goalTurnActive: + 'A goal takes hold on the next turn. Wait for this one to finish before setting one.', + planModeFailedTitle: 'Could not change Plan mode', + planModeFallback: 'Plan mode could not be changed. Try again later.', + orchestrationModeFailedTitle: 'Could not change the orchestration mode', + orchestrationModeFallback: 'The orchestration mode could not be changed. Try again later.', + planModeExitPendingTitle: 'Abandon the current plan?', + planModeExitPendingDescription: (title: string) => + `“${title}” has not been approved. Leaving Plan Mode will mark it as abandoned while preserving its history.`, + planModeExitConfirm: 'Abandon and leave', + planModeExitCancel: 'Keep planning', + planModeExecutionActiveTitle: 'The plan is still running', + planModeExecutionActiveDescription: 'Interrupt the active execution before entering Plan Mode to revise it.', + swarmModeEnabledTitle: 'Swarm Mode is on', + swarmModeDisabledTitle: 'Swarm Mode is off', + swarmModeStatusDescription: 'Use /swarm on, /swarm off, or /swarm for one turn.', + graphModeEnabledTitle: 'Graph Mode is on', + graphModeDisabledTitle: 'Graph Mode is off', + graphModeStatusDescription: 'Use /graph on, /graph off, or /graph for one turn.', + graphHistoryTitle: 'Graph history', + graphHistoryDescription: 'Use the run menu in the Agent Graph panel to inspect history.', + resizeWorkbar: 'Resize task workbar', + }, + } } satisfies UiCatalog; export function getShellCopy(locale: UiLocale): ShellCopy { diff --git a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts index 75ace87c6e..e19913d730 100644 --- a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts @@ -179,7 +179,7 @@ const enCopy: ShellRemainingCopy = { }, }; -const COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog; +const COPY = { zh: zhCopy, en: enCopy, ko: enCopy } satisfies UiCatalog; export function getShellRemainingCopy(locale: UiLocale): ShellRemainingCopy { return COPY[locale]; diff --git a/apps/desktop/src/renderer/settings/bot-chat-detail.tsx b/apps/desktop/src/renderer/settings/bot-chat-detail.tsx index 2e443a042e..8cd1c8435e 100644 --- a/apps/desktop/src/renderer/settings/bot-chat-detail.tsx +++ b/apps/desktop/src/renderer/settings/bot-chat-detail.tsx @@ -40,6 +40,7 @@ import { useMountedRef, useToast, useUiLocale, + type UiLocale, Banner, } from '@maka/ui'; import { PasswordInput } from './password-input'; @@ -653,7 +654,7 @@ function BotAllowedUserIdsField(props: { ); } -function botConnectionLabel(connection: BotStatus['connection'], locale: 'zh' | 'en'): string { +function botConnectionLabel(connection: BotStatus['connection'], locale: UiLocale): string { const copy = getBotSettingsCopy(locale).status; switch (connection) { case 'polling': return copy.polling; diff --git a/apps/desktop/src/renderer/settings/bot-chat-overview.tsx b/apps/desktop/src/renderer/settings/bot-chat-overview.tsx index f3f096fe99..73b15c4e9e 100644 --- a/apps/desktop/src/renderer/settings/bot-chat-overview.tsx +++ b/apps/desktop/src/renderer/settings/bot-chat-overview.tsx @@ -23,7 +23,7 @@ import type { BotChannelSettings, BotProvider } from '@maka/core/bot-chat-settin import type { BotStatus } from '@maka/runtime/bots'; import { BOT_PROVIDERS } from '@maka/core/settings'; import { EmptyState, Item, StatusDot } from '@astryxdesign/core'; -import { Button, RelativeTime, useUiLocale, Banner } from '@maka/ui'; +import { Button, RelativeTime, useUiLocale, Banner, type UiLocale } from '@maka/ui'; import { deriveBotChannelViewState } from './bot-settings-view-model'; import { BOT_LABELS, BotBrandLogo, botReadinessCopyForSupport, botStatusDetail } from './bot-chat-shared'; import { getBotSettingsCopy } from '../locales/settings-bot-copy'; @@ -145,7 +145,7 @@ function botOverviewDetail( currentError: string | undefined, fallback: string, liveOperational: boolean, - locale: 'zh' | 'en', + locale: UiLocale, ): ReactNode { const copy = getBotSettingsCopy(locale).overview; const identity = status?.identity?.username ?? status?.identity?.displayName; diff --git a/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx b/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx index 05d99c09af..843d0bf379 100644 --- a/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx +++ b/apps/desktop/src/renderer/settings/bot-onboarding-modal.tsx @@ -28,6 +28,7 @@ import { Button, useMountedRef, useUiLocale, + type UiLocale, } from '@maka/ui'; import { Dialog, @@ -251,7 +252,7 @@ function statusCopy( starting: boolean, error: string | null, copy: BotSettingsCopy['onboarding']['providers'][BotOnboardingProvider], - locale: 'zh' | 'en' = 'zh', + locale: UiLocale = 'zh', ): string { const shared = getBotSettingsCopy(locale).onboarding; if (starting) return shared.generating; diff --git a/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx b/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx index 57fdc46fcf..d5c96841f4 100644 --- a/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/daily-review-settings-page.tsx @@ -21,7 +21,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Banner } from '@astryxdesign/core'; import type { DailyReviewConfig } from '@maka/core/daily-review'; import type { LlmConnection, ProjectedLlmConnection } from '@maka/core/llm-connections'; -import { Selector, Switch, TextInput, useMountedRef, useUiLocale } from '@maka/ui'; +import { Selector, Switch, TextInput, useMountedRef, useUiLocale, type UiLocale } from '@maka/ui'; import { buildCatalogDailyReviewModelOptions } from '../model-catalog-choices'; import { getDailyReviewSettingsCopy, type DailyReviewSettingsCopy } from '../locales/settings-daily-review-copy'; import { settingsActionErrorMessage } from './settings-error-copy'; @@ -39,7 +39,7 @@ function buildDailyReviewModelOptions( connections: readonly ProjectedLlmConnection[], currentModelKey: string, copy: DailyReviewSettingsCopy, - locale: 'zh' | 'en', + locale: UiLocale, ): Array<{ value: string; label: string }> { return [ { value: DAILY_REVIEW_DEFAULT_MODEL_VALUE, label: copy.defaultModel }, diff --git a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx index 149617a6d0..bcda20fc4c 100644 --- a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx +++ b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx @@ -35,7 +35,7 @@ import { type ProviderType, } from '@maka/core/provider-registry'; import { PROVIDER_REGISTRY, type LlmConnection } from '@maka/core/llm-connections'; -import { Button, TextInput, useUiLocale } from '@maka/ui'; +import { Button, TextInput, useUiLocale, type UiLocale } from '@maka/ui'; import { AddProviderForm } from './provider-add-form'; import { ProviderLogo, providerDisplay } from './provider-display'; import { OAuthLoginPanel, useOAuthCards, type OAuthCardId } from './provider-oauth-section'; @@ -269,7 +269,7 @@ export function ProviderSetupPage(props: { ); } -function providersForCategory(category: CatalogCategory, query: string, locale: 'zh' | 'en'): ProviderType[] { +function providersForCategory(category: CatalogCategory, query: string, locale: UiLocale): ProviderType[] { // 'accounts' is the OAuth-only category: every row in it comes from // useOAuthCards, so the keyed catalog contributes nothing. if (category === 'accounts') return []; diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index 077cf893e9..838c4e311d 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -18,11 +18,10 @@ */ import type { ProviderType } from '@maka/core/llm-connections'; - import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; /** - * Pure-data provider introduction copy, localized zh / en. + * Pure-data provider introduction copy, localized by supported UI locale. * * No React / @maka/ui imports on purpose: the desktop main-process test * runner (node --test over dist/main) imports this module directly, so the @@ -39,269 +38,1079 @@ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; * CATALOG_PROVIDER_TYPES order, with the OAuth account providers at the end. * * Completeness is enforced at compile time: `satisfies Record` fails the build when a registered provider has no bilingual entry. + * …>` fails the build when a registered provider has no locale entry. */ export interface ProviderCopy { name: string; description: string; badge?: string; } - export const UNKNOWN_PROVIDER_DESCRIPTION = { zh: '该 provider 在当前版本未注册。', en: 'This provider is not registered in the current build.', + ko: '현재 빌드에 등록되지 않은 공급자입니다.' } satisfies UiCatalog; - export const PROVIDER_DISPLAY_COPY = { 'kimi-coding-plan': { - zh: { name: 'Kimi Coding Plan', description: '月之暗面 · Anthropic 兼容', badge: 'Coding' }, - en: { name: 'Kimi Coding Plan', description: 'Moonshot · Anthropic-compatible', badge: 'Coding' }, + zh: { + name: 'Kimi Coding Plan', + description: '月之暗面 · Anthropic 兼容', + badge: 'Coding' + }, + en: { + name: 'Kimi Coding Plan', + description: 'Moonshot · Anthropic-compatible', + badge: 'Coding' + }, + ko: { + name: 'Kimi Coding Plan', + description: 'Moonshot · Anthropic-compatible', + badge: 'Coding' + } }, 'minimax-coding-plan': { - zh: { name: 'MiniMax Coding Plan', description: 'MiniMax Coding 套餐 · Anthropic 兼容', badge: 'Coding' }, - en: { name: 'MiniMax Coding Plan', description: 'MiniMax coding plan · Anthropic-compatible', badge: 'Coding' }, + zh: { + name: 'MiniMax Coding Plan', + description: 'MiniMax Coding 套餐 · Anthropic 兼容', + badge: 'Coding' + }, + en: { + name: 'MiniMax Coding Plan', + description: 'MiniMax coding plan · Anthropic-compatible', + badge: 'Coding' + }, + ko: { + name: 'MiniMax Coding Plan', + description: 'MiniMax coding plan · Anthropic-compatible', + badge: 'Coding' + } }, deepseek: { - zh: { name: 'DeepSeek', description: 'DeepSeek 官方接入', badge: 'API' }, - en: { name: 'DeepSeek', description: 'Official DeepSeek API access.', badge: 'API' }, + zh: { + name: 'DeepSeek', + description: 'DeepSeek 官方接入', + badge: 'API' + }, + en: { + name: 'DeepSeek', + description: 'Official DeepSeek API access.', + badge: 'API' + }, + ko: { + name: 'DeepSeek', + description: 'Official DeepSeek API access.', + badge: 'API' + } }, moonshot: { - zh: { name: 'Moonshot', description: 'Moonshot 官方接入', badge: 'API' }, - en: { name: 'Moonshot', description: 'Official Moonshot API access.', badge: 'API' }, + zh: { + name: 'Moonshot', + description: 'Moonshot 官方接入', + badge: 'API' + }, + en: { + name: 'Moonshot', + description: 'Official Moonshot API access.', + badge: 'API' + }, + ko: { + name: 'Moonshot', + description: 'Official Moonshot API access.', + badge: 'API' + } }, 'zai-coding-plan': { - zh: { name: 'Z.AI Coding Plan', description: '智谱 · OpenAI 兼容', badge: 'Coding' }, - en: { name: 'Z.AI Coding Plan', description: 'Zhipu · OpenAI-compatible', badge: 'Coding' }, + zh: { + name: 'Z.AI Coding Plan', + description: '智谱 · OpenAI 兼容', + badge: 'Coding' + }, + en: { + name: 'Z.AI Coding Plan', + description: 'Zhipu · OpenAI-compatible', + badge: 'Coding' + }, + ko: { + name: 'Z.AI Coding Plan', + description: 'Zhipu · OpenAI-compatible', + badge: 'Coding' + } }, MiniMax: { - zh: { name: 'MiniMax', description: 'MiniMax · Anthropic 兼容', badge: 'API' }, - en: { name: 'MiniMax', description: 'MiniMax · Anthropic-compatible', badge: 'API' }, + zh: { + name: 'MiniMax', + description: 'MiniMax · Anthropic 兼容', + badge: 'API' + }, + en: { + name: 'MiniMax', + description: 'MiniMax · Anthropic-compatible', + badge: 'API' + }, + ko: { + name: 'MiniMax', + description: 'MiniMax · Anthropic-compatible', + badge: 'API' + } }, 'MiniMax-cn': { - zh: { name: 'MiniMax 中国站', description: 'MiniMax 中国站 · Anthropic 兼容', badge: 'API' }, - en: { name: 'MiniMax China', description: 'MiniMax China · Anthropic-compatible', badge: 'API' }, + zh: { + name: 'MiniMax 中国站', + description: 'MiniMax 中国站 · Anthropic 兼容', + badge: 'API' + }, + en: { + name: 'MiniMax China', + description: 'MiniMax China · Anthropic-compatible', + badge: 'API' + }, + ko: { + name: 'MiniMax China', + description: 'MiniMax China · Anthropic-compatible', + badge: 'API' + } }, siliconflow: { - zh: { name: 'SiliconFlow', description: '硅基流动多模型 API,支持精确模型 ID。', badge: '聚合' }, - en: { name: 'SiliconFlow', description: 'Hosted multi-model API with exact upstream model ids.', badge: 'Aggregator' }, + zh: { + name: 'SiliconFlow', + description: '硅基流动多模型 API,支持精确模型 ID。', + badge: '聚合' + }, + en: { + name: 'SiliconFlow', + description: 'Hosted multi-model API with exact upstream model ids.', + badge: 'Aggregator' + }, + ko: { + name: 'SiliconFlow', + description: 'Hosted multi-model API with exact upstream model ids.', + badge: 'Aggregator' + } }, anthropic: { - zh: { name: 'Anthropic', description: 'Anthropic 官方接入', badge: 'API' }, - en: { name: 'Anthropic', description: 'Official Anthropic API access.', badge: 'API' }, + zh: { + name: 'Anthropic', + description: 'Anthropic 官方接入', + badge: 'API' + }, + en: { + name: 'Anthropic', + description: 'Official Anthropic API access.', + badge: 'API' + }, + ko: { + name: 'Anthropic', + description: 'Official Anthropic API access.', + badge: 'API' + } }, openai: { - zh: { name: 'OpenAI', description: 'OpenAI 官方接入', badge: 'API' }, - en: { name: 'OpenAI', description: 'Official OpenAI API access.', badge: 'API' }, + zh: { + name: 'OpenAI', + description: 'OpenAI 官方接入', + badge: 'API' + }, + en: { + name: 'OpenAI', + description: 'Official OpenAI API access.', + badge: 'API' + }, + ko: { + name: 'OpenAI', + description: 'Official OpenAI API access.', + badge: 'API' + } }, google: { - zh: { name: 'Google Gemini', description: 'Google AI Studio 接入', badge: 'API' }, - en: { name: 'Google Gemini', description: 'Google AI Studio API access.', badge: 'API' }, + zh: { + name: 'Google Gemini', + description: 'Google AI Studio 接入', + badge: 'API' + }, + en: { + name: 'Google Gemini', + description: 'Google AI Studio API access.', + badge: 'API' + }, + ko: { + name: 'Google Gemini', + description: 'Google AI Studio API access.', + badge: 'API' + } }, xai: { - zh: { name: 'xAI', description: 'xAI 官方接入,Grok 系列模型', badge: 'API' }, - en: { name: 'xAI', description: 'Official xAI API access for Grok models.', badge: 'API' }, + zh: { + name: 'xAI', + description: 'xAI 官方接入,Grok 系列模型', + badge: 'API' + }, + en: { + name: 'xAI', + description: 'Official xAI API access for Grok models.', + badge: 'API' + }, + ko: { + name: 'xAI', + description: 'Official xAI API access for Grok models.', + badge: 'API' + } }, zai: { - zh: { name: 'Z.AI', description: '智谱官方接入,GLM 系列模型', badge: 'API' }, - en: { name: 'Z.AI', description: 'Official Z.AI API access for GLM models.', badge: 'API' }, + zh: { + name: 'Z.AI', + description: '智谱官方接入,GLM 系列模型', + badge: 'API' + }, + en: { + name: 'Z.AI', + description: 'Official Z.AI API access for GLM models.', + badge: 'API' + }, + ko: { + name: 'Z.AI', + description: 'Official Z.AI API access for GLM models.', + badge: 'API' + } }, xiaomi: { - zh: { name: 'Xiaomi', description: '小米官方接入,MiMo 系列模型', badge: 'API' }, - en: { name: 'Xiaomi', description: 'Official Xiaomi API access for MiMo models.', badge: 'API' }, + zh: { + name: 'Xiaomi', + description: '小米官方接入,MiMo 系列模型', + badge: 'API' + }, + en: { + name: 'Xiaomi', + description: 'Official Xiaomi API access for MiMo models.', + badge: 'API' + }, + ko: { + name: 'Xiaomi', + description: 'Official Xiaomi API access for MiMo models.', + badge: 'API' + } }, 'xiaomi-token-plan-cn': { - zh: { name: 'Xiaomi Token Plan 中国', description: '小米 MiMo Token Plan 订阅 · 中国 · 编码工具', badge: 'Token' }, - en: { name: 'Xiaomi Token Plan (China)', description: 'Xiaomi MiMo Token Plan subscription (China) for coding tools.', badge: 'Token' }, + zh: { + name: 'Xiaomi Token Plan 中国', + description: '小米 MiMo Token Plan 订阅 · 中国 · 编码工具', + badge: 'Token' + }, + en: { + name: 'Xiaomi Token Plan (China)', + description: 'Xiaomi MiMo Token Plan subscription (China) for coding tools.', + badge: 'Token' + }, + ko: { + name: 'Xiaomi Token Plan (China)', + description: 'Xiaomi MiMo Token Plan subscription (China) for coding tools.', + badge: 'Token' + } }, 'xiaomi-token-plan-sgp': { - zh: { name: 'Xiaomi Token Plan 新加坡', description: '小米 MiMo Token Plan 订阅 · 新加坡 · 编码工具', badge: 'Token' }, - en: { name: 'Xiaomi Token Plan (Singapore)', description: 'Xiaomi MiMo Token Plan subscription (Singapore) for coding tools.', badge: 'Token' }, + zh: { + name: 'Xiaomi Token Plan 新加坡', + description: '小米 MiMo Token Plan 订阅 · 新加坡 · 编码工具', + badge: 'Token' + }, + en: { + name: 'Xiaomi Token Plan (Singapore)', + description: 'Xiaomi MiMo Token Plan subscription (Singapore) for coding tools.', + badge: 'Token' + }, + ko: { + name: 'Xiaomi Token Plan (Singapore)', + description: 'Xiaomi MiMo Token Plan subscription (Singapore) for coding tools.', + badge: 'Token' + } }, 'xiaomi-token-plan-ams': { - zh: { name: 'Xiaomi Token Plan 欧洲', description: '小米 MiMo Token Plan 订阅 · 欧洲 · 编码工具', badge: 'Token' }, - en: { name: 'Xiaomi Token Plan (Europe)', description: 'Xiaomi MiMo Token Plan subscription (Europe) for coding tools.', badge: 'Token' }, + zh: { + name: 'Xiaomi Token Plan 欧洲', + description: '小米 MiMo Token Plan 订阅 · 欧洲 · 编码工具', + badge: 'Token' + }, + en: { + name: 'Xiaomi Token Plan (Europe)', + description: 'Xiaomi MiMo Token Plan subscription (Europe) for coding tools.', + badge: 'Token' + }, + ko: { + name: 'Xiaomi Token Plan (Europe)', + description: 'Xiaomi MiMo Token Plan subscription (Europe) for coding tools.', + badge: 'Token' + } }, cerebras: { - zh: { name: 'Cerebras', description: '高速推理托管开源模型', badge: 'API' }, - en: { name: 'Cerebras', description: 'Fast hosted open-model inference.', badge: 'API' }, + zh: { + name: 'Cerebras', + description: '高速推理托管开源模型', + badge: 'API' + }, + en: { + name: 'Cerebras', + description: 'Fast hosted open-model inference.', + badge: 'API' + }, + ko: { + name: 'Cerebras', + description: 'Fast hosted open-model inference.', + badge: 'API' + } }, mistral: { - zh: { name: 'Mistral', description: 'Mistral 官方接入', badge: 'API' }, - en: { name: 'Mistral', description: 'Official Mistral API access.', badge: 'API' }, + zh: { + name: 'Mistral', + description: 'Mistral 官方接入', + badge: 'API' + }, + en: { + name: 'Mistral', + description: 'Official Mistral API access.', + badge: 'API' + }, + ko: { + name: 'Mistral', + description: 'Official Mistral API access.', + badge: 'API' + } }, togetherai: { - zh: { name: 'Together AI', description: '托管开源模型 API', badge: 'API' }, - en: { name: 'Together AI', description: 'Hosted open models over one API.', badge: 'API' }, + zh: { + name: 'Together AI', + description: '托管开源模型 API', + badge: 'API' + }, + en: { + name: 'Together AI', + description: 'Hosted open models over one API.', + badge: 'API' + }, + ko: { + name: 'Together AI', + description: 'Hosted open models over one API.', + badge: 'API' + } }, ollama: { - zh: { name: 'Ollama', description: '本机运行 · 离线可用', badge: 'Local' }, - en: { name: 'Ollama', description: 'Runs locally · works offline', badge: 'Local' }, + zh: { + name: 'Ollama', + description: '本机运行 · 离线可用', + badge: 'Local' + }, + en: { + name: 'Ollama', + description: 'Runs locally · works offline', + badge: 'Local' + }, + ko: { + name: 'Ollama', + description: 'Runs locally · works offline', + badge: 'Local' + } }, 'lm-studio': { - zh: { name: 'LM Studio', description: '本机 LM Studio 服务 · 离线可用', badge: 'Local' }, - en: { name: 'LM Studio', description: 'Local models served by LM Studio.', badge: 'Local' }, + zh: { + name: 'LM Studio', + description: '本机 LM Studio 服务 · 离线可用', + badge: 'Local' + }, + en: { + name: 'LM Studio', + description: 'Local models served by LM Studio.', + badge: 'Local' + }, + ko: { + name: 'LM Studio', + description: 'Local models served by LM Studio.', + badge: 'Local' + } }, localai: { - zh: { name: 'LocalAI', description: '本机 LocalAI 服务,可选密钥保护', badge: 'Local' }, - en: { name: 'LocalAI', description: 'Local models served by LocalAI, with optional API key.', badge: 'Local' }, + zh: { + name: 'LocalAI', + description: '本机 LocalAI 服务,可选密钥保护', + badge: 'Local' + }, + en: { + name: 'LocalAI', + description: 'Local models served by LocalAI, with optional API key.', + badge: 'Local' + }, + ko: { + name: 'LocalAI', + description: 'Local models served by LocalAI, with optional API key.', + badge: 'Local' + } }, 'openai-compatible': { - zh: { name: '自定义中转站(OpenAI Chat)', description: 'OpenAI Chat Completions 兼容中转站、代理服务或自部署网关。', badge: '中转' }, - en: { name: 'Custom relay (OpenAI Chat)', description: 'OpenAI Chat Completions-compatible relay, proxy, or self-hosted gateway.', badge: 'Relay' }, + zh: { + name: '自定义中转站(OpenAI Chat)', + description: 'OpenAI Chat Completions 兼容中转站、代理服务或自部署网关。', + badge: '中转' + }, + en: { + name: 'Custom relay (OpenAI Chat)', + description: 'OpenAI Chat Completions-compatible relay, proxy, or self-hosted gateway.', + badge: 'Relay' + }, + ko: { + name: 'Custom relay (OpenAI Chat)', + description: 'OpenAI Chat Completions-compatible relay, proxy, or self-hosted gateway.', + badge: 'Relay' + } }, 'openai-responses-compatible': { - zh: { name: '自定义中转站(OpenAI Responses)', description: 'OpenAI Responses API 兼容中转站、代理服务或自部署网关。', badge: 'Responses' }, - en: { name: 'Custom relay (OpenAI Responses)', description: 'OpenAI Responses-compatible relay, proxy, or self-hosted gateway.', badge: 'Responses' }, + zh: { + name: '自定义中转站(OpenAI Responses)', + description: 'OpenAI Responses API 兼容中转站、代理服务或自部署网关。', + badge: 'Responses' + }, + en: { + name: 'Custom relay (OpenAI Responses)', + description: 'OpenAI Responses-compatible relay, proxy, or self-hosted gateway.', + badge: 'Responses' + }, + ko: { + name: 'Custom relay (OpenAI Responses)', + description: 'OpenAI Responses-compatible relay, proxy, or self-hosted gateway.', + badge: 'Responses' + } }, 'anthropic-compatible': { - zh: { name: '自定义中转站(Anthropic)', description: 'Anthropic Messages 兼容中转站、代理服务或自部署网关。', badge: 'Anthropic' }, - en: { name: 'Custom relay (Anthropic)', description: 'Anthropic Messages-compatible relay, proxy, or self-hosted gateway.', badge: 'Anthropic' }, + zh: { + name: '自定义中转站(Anthropic)', + description: 'Anthropic Messages 兼容中转站、代理服务或自部署网关。', + badge: 'Anthropic' + }, + en: { + name: 'Custom relay (Anthropic)', + description: 'Anthropic Messages-compatible relay, proxy, or self-hosted gateway.', + badge: 'Anthropic' + }, + ko: { + name: 'Custom relay (Anthropic)', + description: 'Anthropic Messages-compatible relay, proxy, or self-hosted gateway.', + badge: 'Anthropic' + } }, 'fireworks-ai': { - zh: { name: 'Fireworks AI', description: 'Serverless 开源模型托管', badge: 'API' }, - en: { name: 'Fireworks AI', description: 'Serverless open models with exact Fireworks model paths.', badge: 'API' }, + zh: { + name: 'Fireworks AI', + description: 'Serverless 开源模型托管', + badge: 'API' + }, + en: { + name: 'Fireworks AI', + description: 'Serverless open models with exact Fireworks model paths.', + badge: 'API' + }, + ko: { + name: 'Fireworks AI', + description: 'Serverless open models with exact Fireworks model paths.', + badge: 'API' + } }, nvidia: { - zh: { name: 'NVIDIA', description: 'NVIDIA 官方托管模型接入', badge: 'API' }, - en: { name: 'NVIDIA', description: 'NVIDIA-hosted models API access.', badge: 'API' }, + zh: { + name: 'NVIDIA', + description: 'NVIDIA 官方托管模型接入', + badge: 'API' + }, + en: { + name: 'NVIDIA', + description: 'NVIDIA-hosted models API access.', + badge: 'API' + }, + ko: { + name: 'NVIDIA', + description: 'NVIDIA-hosted models API access.', + badge: 'API' + } }, 'tencent-tokenhub': { - zh: { name: 'Tencent TokenHub', description: '腾讯云 TokenHub 按量接入,混元等模型', badge: 'API' }, - en: { name: 'Tencent TokenHub', description: 'Tencent Cloud TokenHub pay-as-you-go access.', badge: 'API' }, + zh: { + name: 'Tencent TokenHub', + description: '腾讯云 TokenHub 按量接入,混元等模型', + badge: 'API' + }, + en: { + name: 'Tencent TokenHub', + description: 'Tencent Cloud TokenHub pay-as-you-go access.', + badge: 'API' + }, + ko: { + name: 'Tencent TokenHub', + description: 'Tencent Cloud TokenHub pay-as-you-go access.', + badge: 'API' + } }, stepfun: { - zh: { name: 'StepFun 中国站', description: '阶跃星辰官方接入 · 中国站', badge: 'API' }, - en: { name: 'StepFun (China)', description: 'Official StepFun China API access.', badge: 'API' }, + zh: { + name: 'StepFun 中国站', + description: '阶跃星辰官方接入 · 中国站', + badge: 'API' + }, + en: { + name: 'StepFun (China)', + description: 'Official StepFun China API access.', + badge: 'API' + }, + ko: { + name: 'StepFun (China)', + description: 'Official StepFun China API access.', + badge: 'API' + } }, 'tencent-coding-plan': { - zh: { name: 'Tencent Coding Plan', description: '腾讯云 Coding 套餐 · OpenAI 兼容', badge: 'Coding' }, - en: { name: 'Tencent Coding Plan', description: 'Tencent Cloud coding plan · OpenAI-compatible', badge: 'Coding' }, + zh: { + name: 'Tencent Coding Plan', + description: '腾讯云 Coding 套餐 · OpenAI 兼容', + badge: 'Coding' + }, + en: { + name: 'Tencent Coding Plan', + description: 'Tencent Cloud coding plan · OpenAI-compatible', + badge: 'Coding' + }, + ko: { + name: 'Tencent Coding Plan', + description: 'Tencent Cloud coding plan · OpenAI-compatible', + badge: 'Coding' + } }, 'stepfun-ai': { - zh: { name: 'StepFun 国际站', description: '阶跃星辰官方接入 · 国际站', badge: 'API' }, - en: { name: 'StepFun (Global)', description: 'Official StepFun Global API access.', badge: 'API' }, + zh: { + name: 'StepFun 国际站', + description: '阶跃星辰官方接入 · 国际站', + badge: 'API' + }, + en: { + name: 'StepFun (Global)', + description: 'Official StepFun Global API access.', + badge: 'API' + }, + ko: { + name: 'StepFun (Global)', + description: 'Official StepFun Global API access.', + badge: 'API' + } }, 'volcengine-ark': { - zh: { name: '火山方舟', description: '火山引擎官方接入,豆包等模型', badge: 'API' }, - en: { name: 'Volcengine Ark (China)', description: 'Volcengine Ark direct API access in China.', badge: 'API' }, + zh: { + name: '火山方舟', + description: '火山引擎官方接入,豆包等模型', + badge: 'API' + }, + en: { + name: 'Volcengine Ark (China)', + description: 'Volcengine Ark direct API access in China.', + badge: 'API' + }, + ko: { + name: 'Volcengine Ark (China)', + description: 'Volcengine Ark direct API access in China.', + badge: 'API' + } }, 'volcengine-coding-plan': { - zh: { name: '火山方舟 Coding Plan', description: '火山引擎 Coding 订阅 · OpenAI 兼容', badge: 'Coding' }, - en: { name: 'Volcengine Ark Coding Plan (China)', description: 'Volcengine Ark coding subscription · OpenAI-compatible', badge: 'Coding' }, + zh: { + name: '火山方舟 Coding Plan', + description: '火山引擎 Coding 订阅 · OpenAI 兼容', + badge: 'Coding' + }, + en: { + name: 'Volcengine Ark Coding Plan (China)', + description: 'Volcengine Ark coding subscription · OpenAI-compatible', + badge: 'Coding' + }, + ko: { + name: 'Volcengine Ark Coding Plan (China)', + description: 'Volcengine Ark coding subscription · OpenAI-compatible', + badge: 'Coding' + } }, 'volcengine-agent-plan': { - zh: { name: '火山方舟 Agent Plan', description: '火山引擎 Agent 订阅 · Responses API', badge: 'Agent' }, - en: { name: 'Volcengine Ark Agent Plan (China)', description: 'Volcengine Ark agent subscription · Responses API', badge: 'Agent' }, + zh: { + name: '火山方舟 Agent Plan', + description: '火山引擎 Agent 订阅 · Responses API', + badge: 'Agent' + }, + en: { + name: 'Volcengine Ark Agent Plan (China)', + description: 'Volcengine Ark agent subscription · Responses API', + badge: 'Agent' + }, + ko: { + name: 'Volcengine Ark Agent Plan (China)', + description: 'Volcengine Ark agent subscription · Responses API', + badge: 'Agent' + } }, 'tencent-token-plan': { - zh: { name: 'Tencent Token Plan', description: '腾讯云 Token 套餐,个人智能体与编码工具', badge: 'Token' }, - en: { name: 'Tencent Token Plan', description: 'Tencent Cloud token plan for personal agents and coding tools.', badge: 'Token' }, + zh: { + name: 'Tencent Token Plan', + description: '腾讯云 Token 套餐,个人智能体与编码工具', + badge: 'Token' + }, + en: { + name: 'Tencent Token Plan', + description: 'Tencent Cloud token plan for personal agents and coding tools.', + badge: 'Token' + }, + ko: { + name: 'Tencent Token Plan', + description: 'Tencent Cloud token plan for personal agents and coding tools.', + badge: 'Token' + } }, 'stepfun-step-plan': { - zh: { name: 'StepFun Step Plan 中国站', description: '阶跃星辰订阅套餐 · 中国站', badge: 'Plan' }, - en: { name: 'StepFun Step Plan (China)', description: 'StepFun China subscription for coding and agent tools.', badge: 'Plan' }, + zh: { + name: 'StepFun Step Plan 中国站', + description: '阶跃星辰订阅套餐 · 中国站', + badge: 'Plan' + }, + en: { + name: 'StepFun Step Plan (China)', + description: 'StepFun China subscription for coding and agent tools.', + badge: 'Plan' + }, + ko: { + name: 'StepFun Step Plan (China)', + description: 'StepFun China subscription for coding and agent tools.', + badge: 'Plan' + } }, deepinfra: { - zh: { name: 'DeepInfra', description: '开源模型托管推理 · OpenAI 兼容', badge: 'API' }, - en: { name: 'DeepInfra', description: 'Hosted open-model inference · OpenAI-compatible', badge: 'API' }, + zh: { + name: 'DeepInfra', + description: '开源模型托管推理 · OpenAI 兼容', + badge: 'API' + }, + en: { + name: 'DeepInfra', + description: 'Hosted open-model inference · OpenAI-compatible', + badge: 'API' + }, + ko: { + name: 'DeepInfra', + description: 'Hosted open-model inference · OpenAI-compatible', + badge: 'API' + } }, cohere: { - zh: { name: 'Cohere', description: 'Cohere 官方接入', badge: 'API' }, - en: { name: 'Cohere', description: 'Official Cohere Chat API access.', badge: 'API' }, + zh: { + name: 'Cohere', + description: 'Cohere 官方接入', + badge: 'API' + }, + en: { + name: 'Cohere', + description: 'Official Cohere Chat API access.', + badge: 'API' + }, + ko: { + name: 'Cohere', + description: 'Official Cohere Chat API access.', + badge: 'API' + } }, vercel: { - zh: { name: 'Vercel AI Gateway', description: '一个密钥接入多家托管模型', badge: '网关' }, - en: { name: 'Vercel AI Gateway', description: 'One API key for hosted models with exact creator/model ids.', badge: 'Gateway' }, + zh: { + name: 'Vercel AI Gateway', + description: '一个密钥接入多家托管模型', + badge: '网关' + }, + en: { + name: 'Vercel AI Gateway', + description: 'One API key for hosted models with exact creator/model ids.', + badge: 'Gateway' + }, + ko: { + name: 'Vercel AI Gateway', + description: 'One API key for hosted models with exact creator/model ids.', + badge: 'Gateway' + } }, 'stepfun-ai-step-plan': { - zh: { name: 'StepFun Step Plan 国际站', description: '阶跃星辰订阅套餐 · 国际站', badge: 'Plan' }, - en: { name: 'StepFun Step Plan (Global)', description: 'StepFun Global subscription for coding and agent tools.', badge: 'Plan' }, + zh: { + name: 'StepFun Step Plan 国际站', + description: '阶跃星辰订阅套餐 · 国际站', + badge: 'Plan' + }, + en: { + name: 'StepFun Step Plan (Global)', + description: 'StepFun Global subscription for coding and agent tools.', + badge: 'Plan' + }, + ko: { + name: 'StepFun Step Plan (Global)', + description: 'StepFun Global subscription for coding and agent tools.', + badge: 'Plan' + } }, 'cloudflare-workers-ai': { - zh: { name: 'Cloudflare Workers AI', description: 'Cloudflare 托管模型,账户级接入', badge: 'API' }, - en: { name: 'Cloudflare Workers AI', description: 'Cloudflare-hosted models over the account-scoped API.', badge: 'API' }, + zh: { + name: 'Cloudflare Workers AI', + description: 'Cloudflare 托管模型,账户级接入', + badge: 'API' + }, + en: { + name: 'Cloudflare Workers AI', + description: 'Cloudflare-hosted models over the account-scoped API.', + badge: 'API' + }, + ko: { + name: 'Cloudflare Workers AI', + description: 'Cloudflare-hosted models over the account-scoped API.', + badge: 'API' + } }, huggingface: { - zh: { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家托管模型', badge: '路由' }, - en: { name: 'Hugging Face', description: 'Inference Providers router across hosted models.', badge: 'Router' }, + zh: { + name: 'Hugging Face', + description: 'Inference Providers 路由,聚合多家托管模型', + badge: '路由' + }, + en: { + name: 'Hugging Face', + description: 'Inference Providers router across hosted models.', + badge: 'Router' + }, + ko: { + name: 'Hugging Face', + description: 'Inference Providers router across hosted models.', + badge: 'Router' + } }, 'ollama-cloud': { - zh: { name: 'Ollama Cloud', description: 'Ollama 官方云端托管模型', badge: 'API' }, - en: { name: 'Ollama Cloud', description: 'Ollama-hosted cloud models over the official remote API.', badge: 'API' }, + zh: { + name: 'Ollama Cloud', + description: 'Ollama 官方云端托管模型', + badge: 'API' + }, + en: { + name: 'Ollama Cloud', + description: 'Ollama-hosted cloud models over the official remote API.', + badge: 'API' + }, + ko: { + name: 'Ollama Cloud', + description: 'Ollama-hosted cloud models over the official remote API.', + badge: 'API' + } }, zenmux: { - zh: { name: 'ZenMux', description: '模型路由网关,一个密钥接入多家模型', badge: '网关' }, - en: { name: 'ZenMux', description: 'One API key for routed models with exact creator/model ids.', badge: 'Gateway' }, + zh: { + name: 'ZenMux', + description: '模型路由网关,一个密钥接入多家模型', + badge: '网关' + }, + en: { + name: 'ZenMux', + description: 'One API key for routed models with exact creator/model ids.', + badge: 'Gateway' + }, + ko: { + name: 'ZenMux', + description: 'One API key for routed models with exact creator/model ids.', + badge: 'Gateway' + } }, opencode: { - zh: { name: 'OpenCode Zen', description: '面向编码智能体的按量模型精选', badge: 'Plan' }, - en: { name: 'OpenCode Zen', description: 'Curated pay-as-you-go models for coding agents.', badge: 'Plan' }, + zh: { + name: 'OpenCode Zen', + description: '面向编码智能体的按量模型精选', + badge: 'Plan' + }, + en: { + name: 'OpenCode Zen', + description: 'Curated pay-as-you-go models for coding agents.', + badge: 'Plan' + }, + ko: { + name: 'OpenCode Zen', + description: 'Curated pay-as-you-go models for coding agents.', + badge: 'Plan' + } }, 'opencode-go': { - zh: { name: 'OpenCode Go', description: '低价订阅制的开源编码模型精选', badge: 'Plan' }, - en: { name: 'OpenCode Go', description: 'Low-cost subscription to curated open coding models.', badge: 'Plan' }, + zh: { + name: 'OpenCode Go', + description: '低价订阅制的开源编码模型精选', + badge: 'Plan' + }, + en: { + name: 'OpenCode Go', + description: 'Low-cost subscription to curated open coding models.', + badge: 'Plan' + }, + ko: { + name: 'OpenCode Go', + description: 'Low-cost subscription to curated open coding models.', + badge: 'Plan' + } }, 'opencode-free': { - zh: { name: 'OpenCode Free', description: '免费匿名 OpenCode Zen 模型,无需密钥,按 IP 限速', badge: 'Free' }, - en: { name: 'OpenCode Free', description: 'Free anonymous OpenCode Zen models — no API key, IP-limited.', badge: 'Free' }, + zh: { + name: 'OpenCode Free', + description: '免费匿名 OpenCode Zen 模型,无需密钥,按 IP 限速', + badge: 'Free' + }, + en: { + name: 'OpenCode Free', + description: 'Free anonymous OpenCode Zen models — no API key, IP-limited.', + badge: 'Free' + }, + ko: { + name: 'OpenCode Free', + description: 'Free anonymous OpenCode Zen models — no API key, IP-limited.', + badge: 'Free' + } }, groq: { - zh: { name: 'Groq', description: 'LPU 高速推理托管开源模型', badge: 'API' }, - en: { name: 'Groq', description: 'Ultra-fast LPU-hosted open models.', badge: 'API' }, + zh: { + name: 'Groq', + description: 'LPU 高速推理托管开源模型', + badge: 'API' + }, + en: { + name: 'Groq', + description: 'Ultra-fast LPU-hosted open models.', + badge: 'API' + }, + ko: { + name: 'Groq', + description: 'Ultra-fast LPU-hosted open models.', + badge: 'API' + } }, openrouter: { - zh: { name: 'OpenRouter', description: '一个密钥接入各大模型厂商 · OpenAI 兼容', badge: '聚合' }, - en: { name: 'OpenRouter', description: 'One API key across all major model labs · OpenAI-compatible', badge: 'Aggregator' }, + zh: { + name: 'OpenRouter', + description: '一个密钥接入各大模型厂商 · OpenAI 兼容', + badge: '聚合' + }, + en: { + name: 'OpenRouter', + description: 'One API key across all major model labs · OpenAI-compatible', + badge: 'Aggregator' + }, + ko: { + name: 'OpenRouter', + description: 'One API key across all major model labs · OpenAI-compatible', + badge: 'Aggregator' + } }, alibaba: { - zh: { name: 'Alibaba', description: '阿里云百炼接入,通义千问 Qwen 模型', badge: 'API' }, - en: { name: 'Alibaba', description: 'Alibaba Cloud API access for Qwen models.', badge: 'API' }, + zh: { + name: 'Alibaba', + description: '阿里云百炼接入,通义千问 Qwen 模型', + badge: 'API' + }, + en: { + name: 'Alibaba', + description: 'Alibaba Cloud API access for Qwen models.', + badge: 'API' + }, + ko: { + name: 'Alibaba', + description: 'Alibaba Cloud API access for Qwen models.', + badge: 'API' + } }, 'alibaba-cn': { - zh: { name: 'Alibaba 中国站', description: '阿里云百炼中国站接入,通义千问 Qwen 模型', badge: 'API' }, + zh: { + name: 'Alibaba 中国站', + description: '阿里云百炼中国站接入,通义千问 Qwen 模型', + badge: 'API' + }, en: { name: 'Alibaba (China)', description: 'Alibaba Cloud China-platform API access for Qwen models.', - badge: 'API', + badge: 'API' }, + ko: { + name: 'Alibaba (China)', + description: 'Alibaba Cloud China-platform API access for Qwen models.', + badge: 'API' + } }, 'alibaba-coding-plan-cn': { - zh: { name: 'Alibaba Coding Plan 中国站', description: '阿里云百炼 Coding Plan 订阅 · 中国站', badge: 'Plan' }, - en: { name: 'Alibaba Coding Plan (China)', description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools · China.', badge: 'Plan' }, + zh: { + name: 'Alibaba Coding Plan 中国站', + description: '阿里云百炼 Coding Plan 订阅 · 中国站', + badge: 'Plan' + }, + en: { + name: 'Alibaba Coding Plan (China)', + description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools · China.', + badge: 'Plan' + }, + ko: { + name: 'Alibaba Coding Plan (China)', + description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools · China.', + badge: 'Plan' + } }, 'alibaba-coding-plan': { - zh: { name: 'Alibaba Coding Plan 国际站', description: '阿里云百炼 Coding Plan 订阅 · 国际站', badge: 'Plan' }, - en: { name: 'Alibaba Coding Plan', description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools.', badge: 'Plan' }, + zh: { + name: 'Alibaba Coding Plan 国际站', + description: '阿里云百炼 Coding Plan 订阅 · 国际站', + badge: 'Plan' + }, + en: { + name: 'Alibaba Coding Plan', + description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools.', + badge: 'Plan' + }, + ko: { + name: 'Alibaba Coding Plan', + description: 'Alibaba Cloud Model Studio Coding Plan for interactive coding tools.', + badge: 'Plan' + } }, 'alibaba-token-plan-cn': { - zh: { name: 'Alibaba Token Plan(团队版)', description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 北京', badge: 'Token' }, - en: { name: 'Alibaba Token Plan (China)', description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Beijing region.', badge: 'Token' }, + zh: { + name: 'Alibaba Token Plan(团队版)', + description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 北京', + badge: 'Token' + }, + en: { + name: 'Alibaba Token Plan (China)', + description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Beijing region.', + badge: 'Token' + }, + ko: { + name: 'Alibaba Token Plan (China)', + description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Beijing region.', + badge: 'Token' + } }, 'alibaba-token-plan': { - zh: { name: 'Alibaba Token Plan(团队版)', description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 新加坡', badge: 'Token' }, - en: { name: 'Alibaba Token Plan', description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Singapore region.', badge: 'Token' }, + zh: { + name: 'Alibaba Token Plan(团队版)', + description: '阿里云百炼 Token Plan 订阅,交互式智能体与编码工具 · 新加坡', + badge: 'Token' + }, + en: { + name: 'Alibaba Token Plan', + description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Singapore region.', + badge: 'Token' + }, + ko: { + name: 'Alibaba Token Plan', + description: 'Alibaba Cloud Model Studio Token Plan for interactive agents and coding tools, Singapore region.', + badge: 'Token' + } }, // OAuth account providers (not in CATALOG_PROVIDER_TYPES; shown in the // accounts section and on connection rows). 'github-copilot': { - zh: { name: 'GitHub Copilot', description: 'GitHub Copilot 订阅接入,复用本机 GitHub 登录', badge: 'Account' }, - en: { name: 'GitHub Copilot', description: 'GitHub Copilot subscription access using an existing GitHub login.', badge: 'Account' }, + zh: { + name: 'GitHub Copilot', + description: 'GitHub Copilot 订阅接入,复用本机 GitHub 登录', + badge: 'Account' + }, + en: { + name: 'GitHub Copilot', + description: 'GitHub Copilot subscription access using an existing GitHub login.', + badge: 'Account' + }, + ko: { + name: 'GitHub Copilot', + description: 'GitHub Copilot subscription access using an existing GitHub login.', + badge: 'Account' + } }, 'claude-subscription': { - zh: { name: 'Claude Subscription', description: 'Claude Pro / Max 订阅账号登录;登录后自动成为可用模型连接。' }, - en: { name: 'Claude Subscription', description: 'Sign in with a Claude Pro / Max subscription; it becomes an available model connection once signed in.' }, + zh: { + name: 'Claude Subscription', + description: 'Claude Pro / Max 订阅账号登录;登录后自动成为可用模型连接。' + }, + en: { + name: 'Claude Subscription', + description: 'Sign in with a Claude Pro / Max subscription; it becomes an available model connection once signed in.' + }, + ko: { + name: 'Claude Subscription', + description: 'Sign in with a Claude Pro / Max subscription; it becomes an available model connection once signed in.' + } }, 'openai-codex': { - zh: { name: 'OpenAI OAuth', description: 'ChatGPT / Codex 账号登录;登录后自动成为可用模型连接。' }, - en: { name: 'OpenAI OAuth', description: 'Sign in with a ChatGPT / Codex account; it becomes an available model connection once signed in.' }, + zh: { + name: 'OpenAI OAuth', + description: 'ChatGPT / Codex 账号登录;登录后自动成为可用模型连接。' + }, + en: { + name: 'OpenAI OAuth', + description: 'Sign in with a ChatGPT / Codex account; it becomes an available model connection once signed in.' + }, + ko: { + name: 'OpenAI OAuth', + description: 'Sign in with a ChatGPT / Codex account; it becomes an available model connection once signed in.' + } }, 'xai-oauth': { - zh: { name: 'xAI OAuth', description: '使用 SuperGrok 或 X Premium 账号登录。', badge: 'Account' }, - en: { name: 'xAI OAuth', description: 'Sign in with SuperGrok or X Premium.', badge: 'Account' }, - }, + zh: { + name: 'xAI OAuth', + description: '使用 SuperGrok 或 X Premium 账号登录。', + badge: 'Account' + }, + en: { + name: 'xAI OAuth', + description: 'Sign in with SuperGrok or X Premium.', + badge: 'Account' + }, + ko: { + name: 'xAI OAuth', + description: 'Sign in with SuperGrok or X Premium.', + badge: 'Account' + } + } } satisfies Record>; - export function providerDisplay(type: ProviderType, locale: UiLocale): ProviderCopy { const copy = (PROVIDER_DISPLAY_COPY as Partial>>)[type]?.[locale]; - return copy ?? { name: type, description: UNKNOWN_PROVIDER_DESCRIPTION[locale] }; + if (copy && locale === 'ko') return koreanProviderDisplay(type, copy); + return copy ?? { + name: type, + description: UNKNOWN_PROVIDER_DESCRIPTION[locale] + }; +} + +function koreanProviderDisplay(type: ProviderType, copy: ProviderCopy): ProviderCopy { + const name = copy.name; + const description = copy.description; + const localizedDescription = /API access|API$/u.test(description) + ? `${name} 공식 API 연결` + : /subscription|plan|Token|Plan/u.test(description) + ? `${name} 구독 플랜 연결` + : /locally|Local models|offline/u.test(description) + ? `${name} 로컬 모델 연결` + : /gateway|relay|router|Hosted|hosted/u.test(description) + ? `${name} 호스팅 모델 연결` + : `${name} 모델 연결`; + const badge = copy.badge === 'Coding' + ? '코딩' + : copy.badge === 'Aggregator' + ? '애그리게이터' + : copy.badge === 'Gateway' + ? '게이트웨이' + : copy.badge === 'Responses' + ? 'Responses' + : copy.badge === 'Router' + ? '라우터' + : copy.badge === 'Relay' + ? '릴레이' + : copy.badge === 'Token' + ? '토큰' + : copy.badge === 'Plan' + ? '플랜' + : copy.badge === 'Free' + ? '무료' + : copy.badge === 'Account' + ? '계정' + : copy.badge === 'Local' + ? '로컬' + : copy.badge; + return { name, description: localizedDescription, ...(badge ? { badge } : {}) }; } diff --git a/apps/desktop/src/renderer/settings/providers-panel.tsx b/apps/desktop/src/renderer/settings/providers-panel.tsx index ffa68347b0..3f2412ee38 100644 --- a/apps/desktop/src/renderer/settings/providers-panel.tsx +++ b/apps/desktop/src/renderer/settings/providers-panel.tsx @@ -38,7 +38,7 @@ import { type ProjectedLlmConnection, type ProviderType, } from '@maka/core/llm-connections'; -import { dotForStatus, useMountedRef, useUiLocale } from '@maka/ui'; +import { dotForStatus, useMountedRef, useUiLocale, type UiLocale } from '@maka/ui'; import { settingsActionErrorMessage } from './settings-error-copy'; import { connectionChipStatus } from './provider-connection-status'; import { @@ -634,7 +634,7 @@ function ProvidersPanelContent({ bridge, apiKeyOnboardingBridge, initialPage = ' } /** Provider · default model — the row's second line, and the detail's subtitle. */ -function connectionSubtitle(connection: IdentifiedLlmConnection, locale: 'zh' | 'en'): string { +function connectionSubtitle(connection: IdentifiedLlmConnection, locale: UiLocale): string { const providerName = providerDisplay(connection.providerType, locale).name; const parts = [providerName]; if (connection.defaultModel) parts.push(connection.defaultModel); diff --git a/packages/core/src/__tests__/ui-locale.test.ts b/packages/core/src/__tests__/ui-locale.test.ts index 6fcc097723..5f2f27c390 100644 --- a/packages/core/src/__tests__/ui-locale.test.ts +++ b/packages/core/src/__tests__/ui-locale.test.ts @@ -25,6 +25,7 @@ import { formatUiMessage, isUiLocale, resolveSystemUiLocale, + resolveUiLocale, resolveUiMessageCatalog, uiLocaleToIntlLocale, } from '../ui-locale.js'; @@ -36,11 +37,13 @@ describe('UI message catalogs', () => { detail: { ready: string; waiting: string }; }>()({ en: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, + ko: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, zh: { title: '状态', detail: { ready: '就绪' } }, }); assert.deepEqual(resolveUiMessageCatalog(catalog), { en: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, + ko: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, zh: { title: '状态', detail: { ready: '就绪', waiting: 'Waiting' } }, }); }); @@ -50,6 +53,7 @@ describe('UI message catalogs', () => { assert.equal(formatUiMessage(template, { count: 1 }, 'en'), '1 tool'); assert.equal(formatUiMessage(template, { count: 3 }, 'en'), '3 tools'); + assert.equal(formatUiMessage(template, { count: 1 }, 'ko'), '1 tools'); }); it('fails soft for missing or inherited interpolation values', () => { @@ -64,5 +68,9 @@ describe('UI message catalogs', () => { } const intlLocales = UI_LOCALES.map(uiLocaleToIntlLocale); assert.equal(new Set(intlLocales).size, UI_LOCALES.length); + assert.equal(resolveSystemUiLocale(['ko-KR']), 'ko'); + assert.equal(resolveSystemUiLocale(['ko_KR']), 'ko'); + assert.equal(resolveUiLocale('auto', 'ko'), 'ko'); + assert.equal(uiLocaleToIntlLocale('ko'), 'ko-KR'); }); }); diff --git a/packages/core/src/relative-time.ts b/packages/core/src/relative-time.ts index 5ce6f475bd..cbb25a3e0a 100644 --- a/packages/core/src/relative-time.ts +++ b/packages/core/src/relative-time.ts @@ -50,6 +50,7 @@ const SIDEBAR_TIME_BUCKETS = [ const JUST_NOW: UiCatalog = { zh: '刚刚', en: 'just now', + ko: '방금 전', }; /** Future timestamps are treated as age zero and therefore display as just now. */ diff --git a/packages/core/src/tool-quiet-preview.ts b/packages/core/src/tool-quiet-preview.ts index e2adb0d7b6..2847024378 100644 --- a/packages/core/src/tool-quiet-preview.ts +++ b/packages/core/src/tool-quiet-preview.ts @@ -72,6 +72,16 @@ const STRINGS_BY_LOCALE: Record = { bytes: (n) => `${n} bytes`, moreQuestions: (total) => (total > 1 ? ` +${total - 1} more` : ''), }, + ko: { + backgroundTerminal: '백그라운드 터미널 상호작용', + empty: '(비어 있음)', + done: '완료', + notDone: '미완료', + replacements: (n) => `${n}개 대체`, + written: '작성됨', + bytes: (n) => `${n}바이트`, + moreQuestions: (total) => (total > 1 ? ` +${total - 1}개 더 있음` : ''), + }, }; function strings(locale: UiLocale): QuietPreviewStrings { diff --git a/packages/core/src/ui-locale.ts b/packages/core/src/ui-locale.ts index 3a7d161734..3faa8f1b9a 100644 --- a/packages/core/src/ui-locale.ts +++ b/packages/core/src/ui-locale.ts @@ -20,7 +20,7 @@ import { IntlMessageFormat } from 'intl-messageformat'; /** Resolved locales supported by human-facing Maka clients. */ -export const UI_LOCALES = ['zh', 'en'] as const; +export const UI_LOCALES = ['zh', 'en', 'ko'] as const; export type UiLocale = (typeof UI_LOCALES)[number]; @@ -122,7 +122,7 @@ function isMessageRecord(value: unknown): value is Readonly 48 ? `${normalized.slice(0, 47)}…` : normalized; } -function accessibleActionContext(text: string, ts: number | undefined, locale: 'zh' | 'en'): string { +function accessibleActionContext(text: string, ts: number | undefined, locale: UiLocale): string { return [ accessibleTextExcerpt(text), ts === undefined ? undefined : formatAbsoluteTimestamp(ts, locale), diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 98ae0115c0..4312914911 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -44,7 +44,7 @@ function formatCompactTokenCount(count: number): string { return `${thousands >= 100 ? Math.round(thousands) : Math.round(thousands * 10) / 10}k`; } -/** Wall-clock units for the goal chip's elapsed label, per locale (zh uses spaced words, en letters). */ +/** Wall-clock units for the goal chip's elapsed label, customized per locale. */ interface GoalElapsedUnits { second: string; minute: string; @@ -52,7 +52,7 @@ interface GoalElapsedUnits { day: string; } -/** Humanize a retry delay (seconds) — 1s granularity, locale-aware. zh: `4小时 28分 3秒`, en: `4h 28m 3s`. */ +/** Humanize a retry delay (seconds) — 1s granularity, locale-aware. */ export function formatRetryDelay(seconds: number, locale: UiLocale): string { const s = Math.max(1, Math.ceil(seconds)); const d = Math.floor(s / 86_400); @@ -67,6 +67,14 @@ export function formatRetryDelay(seconds: number, locale: UiLocale): string { if (sec > 0 || parts.length === 0) parts.push(`${sec}秒`); return parts.join(' '); } + if (locale === 'ko') { + const parts: string[] = []; + if (d > 0) parts.push(`${d}일`); + if (h > 0) parts.push(`${h}시간`); + if (m > 0) parts.push(`${m}분`); + if (sec > 0 || parts.length === 0) parts.push(`${sec}초`); + return parts.join(' '); + } const parts: string[] = []; if (d > 0) parts.push(`${d}d`); if (h > 0) parts.push(`${h}h`); @@ -75,7 +83,7 @@ export function formatRetryDelay(seconds: number, locale: UiLocale): string { return parts.join(' '); } -/** One shared elapsed ladder so the zh/en goalElapsed entries cannot drift. */ +/** One shared elapsed ladder so the locale-specific goalElapsed entries cannot drift. */ function formatGoalElapsedUnits(elapsedMs: number, units: GoalElapsedUnits): string { const seconds = Math.max(0, Math.floor(elapsedMs / 1000)); if (seconds < 60) return `${seconds}${units.second}`; @@ -732,6 +740,164 @@ const CONVERSATION_COPY = { listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: (name) => `${name} task actions`, pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Task grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, pickedAriaLabel: 'Selected', pinCount: (count) => `Pin ${count} tasks`, unpinCount: (count) => `Unpin ${count} tasks`, archiveCount: (count) => `Archive ${count} tasks`, }, }, + ko: { + empty: { + ariaLabel: 'Start a task', + surfaceAriaLabel: 'New task conversation', + greeting: { morning: 'Good morning', noon: 'Good afternoon', afternoon: 'Good afternoon', evening: 'Good evening' }, + greetingTail: { morning: 'A clear morning is good for untangling ideas', noon: 'A focused midday is good for a single big push', afternoon: 'A calm afternoon is good for steady progress', evening: 'A quiet evening is good for deep thinking' }, + headlineWithLabel: (greeting, label) => `${greeting} ${label} — what shall we tackle today?`, headlineFallback: (greeting, tail) => `${greeting} — ${tail}.`, + }, + deepResearchEmpty: { + ariaLabel: 'Empty Deep Research task', eyebrow: 'Deep Research · Read-only exploration', title: 'Understand the project before deciding what to change.', intro: 'This task stays read only: inspect, search, and analyze first. When implementation is needed, report the files, risks, and verification commands.', + workflowAriaLabel: 'Deep Research workflow', workflow: [ + { title: 'Find the entry points', body: 'Read the directory layout, configuration, startup path, and test entry points to build a project map.' }, + { title: 'Trace the data flow', body: 'Follow key modules through IPC, storage, permissions, and runtime boundaries to the real implementation.' }, + { title: 'Compare references', body: 'Break each reusable idea into borrow / diverge / risk / gate.' }, + { title: 'Propose a mergeable plan', body: 'List files, risk boundaries, and verification commands without changing files in read-only mode.' }, + ], + reportAriaLabel: 'Deep Research report structure', reportTitle: 'The report must be actionable', report: [ + { title: 'Lead with conclusions', body: 'Use three to five points to explain the current state, major gaps, and priorities.' }, + { title: 'Cite source evidence', body: 'Name files, functions, configuration, tests, and runtime paths instead of relying on impressions.' }, + { title: 'Break down what to borrow', body: 'Describe each idea as borrow / diverge / risk / gate.' }, + { title: 'Make it implementable', body: 'Give a small-step file plan, boundaries, and verification commands.' }, + ], + scopeAriaLabel: 'Deep Research scope', scopeTitle: 'Standard depth by default', scope: [ + { label: 'Quick', body: 'Scan entry points, key files, and the likeliest data flow for a narrowly scoped question.' }, + { label: 'Standard', body: 'Trace the core path, related tests, and major risks before recommending changes.' }, + { label: 'Deep', body: 'Run multi-pass investigation across modules, references, and edge cases only when explicitly requested.' }, + ], + evidenceAriaLabel: 'Deep Research evidence checklist', evidenceTitle: 'Leave evidence for every investigation', evidence: [ + { title: 'Project entry points', body: 'Check the README, package/config files, startup scripts, and directory layers to confirm how the project runs.' }, + { title: 'Core path', body: 'Trace UI entry points, IPC/services, storage, runtime calls, and error handling.' }, + { title: 'Boundaries', body: 'Check permissions, privacy mode, token/path exposure, retries, and user-visible feedback.' }, + { title: 'Verification evidence', body: 'Find tests, fixtures, smoke documentation, and reproducible commands; call out missing evidence.' }, + ], + progressAriaLabel: 'Deep Research checkpoints', progressTitle: 'Advance multi-step research through checkpoints', progress: [ + { title: 'Build a checklist', body: 'When the scope has more than three related areas, list verifiable checks before tracing code.' }, + { title: 'Mark the current check', body: 'State what is being verified and move on only after collecting evidence.' }, + { title: 'Record blockers', body: 'Mark missing source, runtime, or test evidence as blocked instead of guessing.' }, + { title: 'Converge on a plan', body: 'Roll completed checks into borrow / diverge / risk / gate and actionable improvements.' }, + ], + startersAriaLabel: 'Deep Research starters', starters: [ + { label: 'Research a reference project', prompt: 'Read this project without changing files. Map its structure, core modules, startup path, data flow, and tests; then list reusable design ideas, risks, and an implementation order for Maka.' }, + { label: 'Read a reference project end to end', prompt: 'Perform a deep, read-only study of this project. Map modules and trace core features, runtime, storage, permissions, UI, tests, and docs. Express each idea as borrow / diverge / risk / gate and recommend an implementation order for Maka.' }, + { label: 'Compare a feature implementation', prompt: 'Compare this feature in the reference project and Maka without changing files. Identify key files, runtime boundaries, UI entry points, persistence, tests, and the smallest mergeable improvement.' }, + { label: 'Audit security boundaries', prompt: 'Audit this feature read only: permissions, token and secret flow, IPC/renderer exposure, file paths, privacy mode, logs, and telemetry. Report blocking risks and corresponding contract tests.' }, + ], + }, + composer: { + placeholder: 'Describe a task, @ to reference files, / for skills…', textareaAriaLabel: 'Message input', pastedQuoteLabel: 'Pasted text', selectedSkillsAriaLabel: 'Selected Skills', removeSkillAriaLabel: (name) => `Remove Skill: ${name}`, awaitingPermission: 'Waiting for your permission decision…', + sending: 'Sending…', importing: 'Importing…', sendLabel: 'Send', + queuedMessagesAriaLabel: (count) => `${count} queued message${count === 1 ? '' : 's'}`, + promoteQueuedEntry: 'Steer', editQueuedEntry: 'Edit', saveQueuedEntry: 'Save', cancelQueuedEntryEdit: 'Cancel editing', deleteQueuedEntry: 'Delete', reorderQueuedEntry: 'Drag to reorder', + stopLabel: 'Stop', stopping: 'Stopping…', + streaming: 'Maka is responding…', processing: 'Maka is working…', continuing: 'Maka is continuing…', + interruptHint: 'or click Stop to interrupt', addContext: 'Add context', stagedContext: 'staged items', + selectModel: 'Choose model', dropToImport: 'Drop to import file contents', addingAttachment: 'Adding attachment', addFileOrDirectory: 'Add files', referenceFolder: 'Reference folder', + chooseSkill: 'Choose skills', noSkillsAvailable: 'No skills available', + setGoal: 'Set a goal…', goalAlreadySet: 'This session already has a goal in progress', + switchDisabledStreaming: 'Wait for the current response to finish before switching models.', switchDisabledRunning: 'Wait for the current run to finish before switching models.', switchDisabledPermission: 'Resolve the pending tool permission before switching models.', + thinkingDisabledStreaming: 'Wait for the current response to finish before changing the thinking level.', thinkingDisabledRunning: 'Wait for the current run to finish before changing the thinking level.', thinkingDisabledPermission: 'Resolve the pending tool permission before changing the thinking level.', + orchestrationModeAriaLabel: 'Orchestration mode', + planModeLabel: 'Plan', enablePlanMode: 'Enable Plan Mode', disablePlanMode: 'Disable Plan Mode', + planModeOnTitle: 'Plan mode is on — click to turn off', + swarmModeLabel: 'Swarm', swarmModeOnTitle: 'Swarm mode is on — click to turn off', + graphModeLabel: 'Graph', graphModeOnTitle: 'Graph mode is on — click to turn off', + noModelHint: 'No model connection yet, so sending is unavailable.', noModelAction: 'Go to model settings', noModelSendTitle: 'Add a model connection before sending.', + }, + model: { + thinkingLevel: 'Thinking level', thinkingUnsupported: 'This model does not support thinking-level changes', changeThinkingLevel: 'Change the current model thinking level', defaultLevel: 'Model default', + level: { off: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Maximum' }, + switching: 'Switching', model: 'Model', switchAriaLabel: 'Switch model for this task', + switchWarning: 'Switching may rebuild the provider prompt cache, making the next request slower or more expensive.', + newChatAriaLabel: (label) => `Choose a model for the new task, currently ${label}`, newChatTitle: (label) => `Model for the new task: ${label}`, + configureAriaLabel: (label) => `Configure model connections, currently ${label}`, configureTitle: 'Configure model connections', + }, + permissions: { + mode: { + explore: { label: 'Read only', hint: 'Read and search only; asks before write or network.' }, + ask: { label: 'Auto', hint: "Runs inside Maka's protection; asks before going further." }, + bypass: { label: 'Full access', hint: 'Direct file and network access. Trust-only tasks.' }, + }, + modeAriaLabel: (label) => `Permission mode: ${label}`, + }, + sandboxBoundary: { + title: 'Allow access outside the workspace?', + access: { read: 'Read', write: 'Write' }, + scope: { exact: 'Exact path', subtree: 'Directory subtree' }, + network: 'Network access', + enabled: 'Enabled', + reject: 'Reject', + allowSession: 'Allow for this task', + }, + clientCapability: { + title: 'Allow this client capability?', + browser: (origin) => `Allow Browser to operate ${origin}`, + computerUse: 'Allow Computer Use to operate this Mac', + desktopMcp: (serverId, toolName) => `Allow ${toolName} from ${serverId}`, + sessionNotice: 'Matching operations will be allowed for the rest of this task.', + reject: 'Reject', + allowSession: 'Allow for this task', + }, + questions: { other: 'Other', otherDescription: 'Enter a different answer.', otherAriaLabel: 'Other answer', otherPlaceholder: 'Enter your answer', stop: 'Stop', stopping: 'Stopping…', previous: 'Previous', submitting: 'Submitting…', submit: 'Submit answers', next: 'Next' }, + mentions: { noFiles: 'No files found', noSkills: 'No skills available', noCommandsOrSkills: 'No matching commands or skills', filesAriaLabel: 'Workspace files', skillsAriaLabel: 'Skills', commandsAndSkillsAriaLabel: 'Commands and skills', commandsGroup: 'Commands', skillsGroup: 'Skills', loading: 'Loading…' }, + workspace: { + choose: 'Choose project', current: 'Current project', addProject: 'Add project', manageProjects: 'Manage projects', noProject: 'No project', relink: 'Relink', unavailable: 'Unavailable', + chooseTitle: (branch) => branch ? `Choose project · ${branch}` : 'Choose project', + chooseAriaLabel: (label, branch) => branch ? `Choose project: ${label}, current branch ${branch}` : `Choose project: ${label}`, + }, + messages: { + you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, 'en')} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `Waiting to retry (${attempt}/${maxAttempts})`, providerRetryReason: { network: 'Network interrupted', provider_capacity: 'The model service is temporarily at capacity', provider_unavailable: 'Model service temporarily unavailable', rate_limit: 'Model rate limit reached', timeout: 'Request timed out', unknown: 'Model request failed' }, safeResumePending: 'Checking…', safeResume: 'Continue this turn', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages with expanded context', + editMessageDisabledDirectoryReferences: 'Edit & resend does not yet support messages with folder references', + userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: (context) => `Response actions${context ? `: ${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action} response${context ? `: ${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action} message${context ? `: ${context}` : ''}`, sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this one', scheduledTaskTriggered: 'Triggered by scheduled task', scheduledTaskTitle: (id) => `Triggered by scheduled task · ${id}`, legacyAutomationTriggered: 'Legacy Automation (history only)', legacyAutomationTitle: (id) => `Triggered by legacy Automation · ${id} · Historical only; it will not run again`, goalContinued: 'Continued by Goal', goalTitle: (id) => `Continued by Goal · ${id}`, agentGraphTriggered: 'Continued by Agent Graph', agentGraphTitle: (graphId) => `Triggered by the Agent Graph scheduler · ${graphId}`, + thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: 'Interrupted', abortedByStop: 'Interrupted · Stop button', + systemNotes: { + contextCompacted: 'Context compacted to keep this session within the model window.', + contextCompactionFailedOpen: 'Context summary failed; the session continued without a new summary.', + stepLimit: 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.', + }, + }, + chat: { + conversationAriaLabel: (name) => `Conversation: ${name}`, + memory: 'Memory', memoryAriaLabel: 'Local memory enabled', memoryTitle: 'Local MEMORY.md is included in the agent system prompt. Click to manage it in Settings · Memory.', deepResearch: 'Deep Research', deepResearchAriaLabel: 'Deep Research, read-only exploration', deepResearchTitle: 'Deep Research uses a read-only boundary: inspect and analyze first, without changing files by default.', + deepResearchProgress: { + ariaLabel: 'Live Deep Research progress', + title: 'Research progress', + completedSummary: 'Research complete · Original task remains read-only', + activeSummary: (stage, scope, round) => `${stage} · ${scope} · Round ${round}`, + handoffTitle: 'Create a normal task with the research handoff. It will not send automatically or change the original research task permissions.', + handoffAction: 'Continue implementation in a new task', + checklistTitle: 'Checklist', + reportTitle: 'Report draft', + inspectedTitle: 'Inspected locations', + inspectedEmpty: 'Waiting for recorded files, symbols, or sources.', + executionTitle: 'Execution and blockers', + executionSummary: (steps, artifacts) => `${steps} research steps · ${artifacts} persisted evidence items`, + workersLabel: 'Workers', + noBlockers: 'No current blockers.', + sectionLabels: { + conclusion: 'Conclusion', + source_evidence: 'Evidence', + borrow_diverge_risk_gate: 'Tradeoffs and risks', + implementation_recommendations: 'Implementation recommendations', + verification: 'Verification', + }, + }, + clearGoal: (condition, iteration, max, status) => `Autonomous goal in progress: “${condition}” (iteration ${iteration}/${max}, ${status}). Maka continues after each iteration; click to clear the goal and stop continuing.`, clearGoalAriaLabel: (iteration, max) => `Clear autonomous goal after ${iteration}/${max} iterations`, goalProgress: (iteration, max) => `Goal ${iteration} of ${max}`, goalRunningAriaLabel: 'Autonomous goal running', goalWaitingAriaLabel: 'Autonomous goal waiting for conditions to change', + goalPausedAriaLabel: 'Autonomous goal paused', pauseGoalAriaLabel: (iteration, max) => `Pause autonomous goal after ${iteration}/${max} iterations`, resumeGoalAriaLabel: (iteration, max) => `Resume autonomous goal after ${iteration}/${max} iterations`, pauseGoal: (condition, iteration, max, status) => `Pause autonomous goal: “${condition}” (iteration ${iteration}/${max}, ${status}). Pausing stops autonomous continuation immediately — no more tokens burn; resume any time.`, resumeGoal: (condition, iteration, max) => `Resume autonomous goal: “${condition}” (iteration ${iteration}/${max}). Resuming continues autonomous iteration immediately.`, goalElapsed: (elapsedMs) => formatGoalElapsedUnits(elapsedMs, { second: 's', minute: 'm', hour: 'h', day: 'd' }), goalTokens: (spent, budget) => `${formatCompactTokenCount(spent)} / ${formatCompactTokenCount(budget)}`, + loadFailed: 'Task failed to load', loading: 'Loading…', retryLoad: 'Retry', quoteSelection: 'Quote', askInSidePanel: 'Ask in side panel', noMessages: 'No messages yet', + branchBeforeInterrupt: 'Branched before interruption', sessionContextAriaLabel: 'Task context', sessionLineageAriaLabel: 'Task origin', sessionContextMore: (count) => `More task context (${count})`, + titlebarIdentityAriaLabel: 'Current task', openProjectFolder: (name) => `Open “${name}” in the file manager`, openProjectFolderAction: 'Open project folder', + openParentSession: (name) => `Return to parent task “${name}”`, openParentSessionAction: 'Open parent task', + revisionVersionsAriaLabel: 'Task versions', revisionVersion: (current, total) => `Version ${current} of ${total}`, previousRevision: 'View previous version', nextRevision: 'View next version', + }, + sessions: { + status: { active: 'Ready', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Needs attention', aborted: 'Stopped' }, + blockedReason: { NO_REAL_CONNECTION: 'Waiting for an available model connection', auth: 'Sign in again', permission_required: 'Waiting for permission', tool_failed: 'Tool call failed', unknown: 'Run interrupted; retry available' }, + listAriaLabel: 'Task list', showMore: 'Show more', showMoreAriaLabel: (count) => `Show ${count} more tasks`, renameAriaLabel: 'Rename task', renameProjectTitle: 'Rename project', renameSubmit: 'Save', respondingAriaLabel: 'Responding', respondingTitle: 'This task is streaming a response', staleTitle: 'This task\'s model connection is unavailable; sending will switch to the default connection', staleAriaLabel: 'Stale task', stale: 'Stale', unreadAriaLabel: 'Unread messages', actionsAriaLabel: (name) => `${name} task actions`, pin: 'Pin', unpin: 'Unpin', rename: 'Rename', archive: 'Archive', unarchive: 'Unarchive', delete: 'Delete', pinned: 'Pinned', recent: 'Recent', groupByTime: 'By time', groupByProject: 'By project', groupingAriaLabel: 'Task grouping', projectActionsAriaLabel: (name) => `${name} project actions`, projectNewTask: 'New task', projectRename: 'Rename', projectArchive: 'Archive', projectRestore: 'Restore', projectRelink: 'Relocate', projectUnavailable: 'Project directory unavailable', archivedProjects: 'Archived projects', archivedProjectsAriaLabel: 'Expand archived projects', worktreeAriaLabel: 'Git worktree', promptRailAriaLabel: 'Jump by prompt', emptyPrompt: '(empty prompt)', jumpToPrompt: (preview) => `Jump to prompt: ${preview}`, pickedAriaLabel: 'Selected', pinCount: (count) => `Pin ${count} tasks`, unpinCount: (count) => `Unpin ${count} tasks`, archiveCount: (count) => `Archive ${count} tasks`, + }, + } } satisfies UiCatalog; export function getConversationCopy(locale: UiLocale): ConversationCopy { diff --git a/packages/ui/src/daily-review-copy.ts b/packages/ui/src/daily-review-copy.ts index 3afe0196f8..0557a0c293 100644 --- a/packages/ui/src/daily-review-copy.ts +++ b/packages/ui/src/daily-review-copy.ts @@ -174,6 +174,41 @@ const DAILY_REVIEW_COPY = { separator: ':', title: (dayLabel) => `# Maka · Daily review · ${dayLabel}`, conversations: 'Tasks', requests: 'Model calls', tokens: 'Tokens', cost: 'Cost', errors: 'Errors', activeConversations: 'Active tasks', modelUsage: 'Model usage', toolCalls: 'Tool calls', requestCount: (count) => `${count} ${count === 1 ? 'call' : 'calls'}`, }, }, + ko: { + archive: { + section: { summary: 'Task summary', gaps: 'Missed items', usage: 'Usage insights', code: 'Code suggestions' }, + status: { ok: 'Generated', no_model: 'Model unavailable', no_data: 'No data', failed: 'Generation failed', skipped: 'Skipped' }, + trigger: { cron: 'Scheduled', manual: 'Manual' }, + title: (date, mode) => `${date} · ${mode}`, + range: { 1: '1 day', 7: '7 days', 30: '30 days' }, + generated: (trigger, time) => `${trigger} · ${time}`, + sessionCount: (count) => `${count} ${count === 1 ? 'task' : 'tasks'}`, + defaultModel: 'Default task model', + opening: 'Opening this report…', + noContent: 'This report has no generated content.', + noContentHelp: 'Nothing archived for this day.', + }, + date: { + today: 'Today', yesterday: 'Yesterday', daysAgo: (count) => `${count} days ago`, recent7Days: 'Last 7 days', recent30Days: 'Last 30 days', shiftedRange: (range, days) => `${range} (${days} days earlier)`, + unit: { day: 'day', week: 'week', month: 'month' }, earlier: (unit) => `View previous ${unit}`, later: (unit) => `View next ${unit}`, + }, + emptyOverview: { + todayTitle: "Waiting for today's activity", rangeTitle: (label) => `No activity for ${label.toLowerCase()}`, todayBody: 'No tasks or model requests have started today.', rangeBody: (label) => `No tasks or model requests were made during ${label.toLowerCase()}.`, + }, + export: { + ariaLabel: 'Review export actions', copyTitle: 'Copy a Markdown summary to share or add to notes', copying: 'Copying…', copy: 'Copy', appendTitle: 'Append to the current composer draft', appending: 'Appending…', append: 'Add to composer', saveTitle: 'Save as a Markdown file', saving: 'Saving…', save: 'Save', + }, + page: { + title: 'Daily review', generateAnalysis: 'Generate analysis', retryAnalysis: 'Generate again', viewAnalysis: 'View analysis', backToActivity: 'Back to activity', timeRange: 'Time range', rangeOptions: [['1', 'Today'], ['7', 'Last 7 days'], ['30', 'Last 30 days']], rangeSwitch: 'Change time range', + }, + overview: { + ariaLabel: (label) => `${label} overview`, refreshFailed: (error) => `Failed to refresh daily review: ${error}`, retry: 'Retry', conversations: 'Tasks', requests: 'Model calls', tokens: 'Tokens', cost: 'Cost', activeConversations: 'Active tasks', + }, + errorFallback: 'Daily review is temporarily unavailable. Try again later.', + markdown: { + separator: ':', title: (dayLabel) => `# Maka · Daily review · ${dayLabel}`, conversations: 'Tasks', requests: 'Model calls', tokens: 'Tokens', cost: 'Cost', errors: 'Errors', activeConversations: 'Active tasks', modelUsage: 'Model usage', toolCalls: 'Tool calls', requestCount: (count) => `${count} ${count === 1 ? 'call' : 'calls'}`, + }, + } } satisfies UiCatalog; export function getDailyReviewCopy(locale: UiLocale): DailyReviewCopy { diff --git a/packages/ui/src/locale-helpers.ts b/packages/ui/src/locale-helpers.ts index 5340e7400d..91522405c5 100644 --- a/packages/ui/src/locale-helpers.ts +++ b/packages/ui/src/locale-helpers.ts @@ -86,6 +86,14 @@ const PROMPT_SUGGESTIONS_BY_LOCALE: UiCatalog = { { label: 'Draft message', prompt: 'Help me draft a ____ message to ____, with the goal of ____:\n\nPoints to cover:\n- \n- ' }, { label: 'Review code', prompt: 'Please review this code — readability, error handling, performance concerns:\n\n```\n\n```' }, ], + ko: [ + { label: '코드베이스 요약', prompt: '현재 코드베이스의 디렉터리 구조와 주요 모듈을 요약해 주세요.' }, + { label: '코드 설명', prompt: '코드 조각을 붙여넣을게요. 한 줄씩 설명하고 주의할 점을 알려 주세요:\n\n```\n\n```' }, + { label: '긴 문서 읽기', prompt: '이 글이나 문서의 핵심 주장과 주요 사실을 정리하고 빠진 부분을 알려 주세요:\n\n' }, + { label: '번역 및 다듬기', prompt: '아래 텍스트를 자연스럽고 전문적인 한국어로 번역해 주세요. 의미는 유지하세요:\n\n' }, + { label: '메시지 초안', prompt: '목표가 ____인 ____에게 보낼 메시지 초안을 작성해 주세요:\n\n포함할 내용:\n- \n- ' }, + { label: '코드 리뷰', prompt: '이 코드의 가독성, 오류 처리 및 성능 문제를 검토해 주세요:\n\n```\n\n```' }, + ], }; export function getPromptSuggestions(locale: UiLocale): PromptSuggestion[] { diff --git a/packages/ui/src/runtime-resume-copy.ts b/packages/ui/src/runtime-resume-copy.ts index 002e2ef6ce..267d08ba5f 100644 --- a/packages/ui/src/runtime-resume-copy.ts +++ b/packages/ui/src/runtime-resume-copy.ts @@ -69,7 +69,7 @@ interface ResumeParkCopy { reasons: ResumeParkReasonCopy; } -const RESUME_PARK_COPY = { +const RESUME_PARK_COPY_BASE = { zh: { title: '暂时无法继续这一轮', fallbackDescription: '当前任务不满足继续的条件。', @@ -150,7 +150,20 @@ const RESUME_PARK_COPY = { resume_feature_disabled: 'Resuming interrupted tasks is not enabled.', }, }, -} satisfies UiCatalog; + ko: { + title: '현재 이 작업을 계속할 수 없습니다', + fallbackDescription: '이 작업은 계속하기 위한 조건을 충족하지 않습니다.', + missingCandidateTitle: '복구할 작업이 없습니다', + missingCandidateDescription: '작업이 이미 최신 상태입니다.', + reasons: { + dangling_tool_state: '도구 상태가 완료되지 않았습니다.', pending_permission: '대기 중인 권한 요청이 있습니다.', background_operation_pending: '백그라운드 작업이 아직 진행 중입니다.', workspace_identity_mismatch: '작업 공간 ID가 일치하지 않습니다.', + workspace_identity_missing: '중단된 실행의 작업 공간을 확인할 수 없습니다.', workspace_cwd_mismatch: '현재 작업 디렉터리가 중단 당시와 일치하지 않습니다.', workspace_ref_missing: '중단 당시 작업 공간을 더 이상 사용할 수 없습니다.', tool_catalog_mismatch: '사용 가능한 도구가 변경되어 안전하게 계속할 수 없습니다.', checkpoint_restore_failed: '작업 공간 체크포인트를 복원하지 못했습니다.', source_run_unreadable: '이전 실행 기록을 완전히 읽을 수 없습니다.', runtime_ledger_unreadable: '이전 실행 원장을 완전히 읽을 수 없습니다.', runtime_ledger_empty: '이전 실행에 재생할 기록이 없습니다.', terminal_repair_failed: '이전 실행 기록을 복구하지 못했습니다.', provider_resume_head_unsupported: '현재 모델은 이 복구 지점을 지원하지 않습니다.', provider_resume_boundary_unsupported: '현재 모델은 이 복구 경계를 지원하지 않습니다.', provider_replay_non_suffix_gap: '이전 모델 출력의 중단 지점을 안전하게 잘라낼 수 없습니다.', provider_replay_unsupported: '현재 모델 프로토콜에서 이전 실행 기록을 안전하게 재생할 수 없습니다.', runtime_lineage_cycle: '복구 체인에 순환 참조가 있어 복구를 중지했습니다.', runtime_lineage_depth_exceeded: '복구 체인이 너무 길어 자동 복구를 중지했습니다.', runtime_lineage_missing: '복구 체인에 필요한 기록이 없습니다.', runtime_lineage_start_mismatch: '복구 체인의 시작 기록이 일치하지 않습니다.', runtime_lineage_replay_mismatch: '복구 체인의 모델 컨텍스트가 현재 재구성 결과와 일치하지 않습니다.', + runtime_lineage_claim_mismatch: '복구 소유권 기록이 일치하지 않습니다.', source_prefix_digest_mismatch: '이전 실행의 변경 불가 경계가 변경되었습니다.', continuation_already_exists: '이 중단된 작업에 대한 계속 실행이 이미 있습니다.', continuation_claim_repair_required: '계속 실행 기록을 먼저 복구해야 합니다.', continuation_started_indeterminate: '계속 실행이 시작되었지만 종료 상태를 확인할 수 없습니다.', continuation_authority_unavailable: '현재 저장소는 안전한 계속 실행 소유권을 지원하지 않습니다.', resume_feature_disabled: '중단된 작업 계속 실행이 활성화되지 않았습니다.', + }, + }, +}; + +const RESUME_PARK_COPY = RESUME_PARK_COPY_BASE satisfies UiCatalog; export function resumeParkToastCopy(reasons: readonly string[], locale: UiLocale): ResumeParkToastCopy { const copy = RESUME_PARK_COPY[locale]; diff --git a/packages/ui/src/scheduled-task-copy.ts b/packages/ui/src/scheduled-task-copy.ts index 6ef0e41ec9..91abd1044b 100644 --- a/packages/ui/src/scheduled-task-copy.ts +++ b/packages/ui/src/scheduled-task-copy.ts @@ -241,6 +241,42 @@ const SCHEDULED_TASK_COPY = { agentDelivery: 'Run via the Agent', }, }, + ko: { + templates: [ + { id: 'daily-download-cleanup', title: 'Clean up Downloads', note: 'Organize screenshots, installers, and temporary documents in Downloads by type, then list items that can be deleted.', scheduleLabel: 'Daily at 18:30', recurrence: 'cron', cronExpression: '30 18 * * *', nextRun: { hour: 18, minute: 30 } }, + { id: 'midday-reset', title: 'Midday reset', note: 'Review what I completed this morning and create a lightweight, actionable plan for the afternoon.', scheduleLabel: 'Weekdays at 12:30', recurrence: 'cron', cronExpression: '30 12 * * 1-5', nextRun: { hour: 12, minute: 30 } }, + { id: 'weekend-todo-review', title: 'Weekend task review', note: 'Review completed and unfinished tasks from this week, outline next week, and flag the three highest priorities.', scheduleLabel: 'Sundays at 20:00', recurrence: 'cron', cronExpression: '0 20 * * 0', nextRun: { weekday: 0, hour: 20, minute: 0 } }, + { id: 'daily-news-brief', title: 'Daily news brief', note: 'Summarize five important technology, AI, or Maka stories from today and add one sentence about the impact of each.', scheduleLabel: 'Daily at 09:30', recurrence: 'cron', cronExpression: '30 9 * * *', nextRun: { hour: 9, minute: 30 } }, + ], + validation: { title: 'Add a title before saving this task.', timeInvalid: 'Choose a valid task time.', timePast: 'The task time must be in the future.', cron: 'Cron expressions need five fields, for example 0 9 * * 1-5.', chatId: 'Enter a Chat ID when delivering to a bot chat.' }, + status: { active: 'Scheduled', paused: 'Paused', completed: 'Completed', expired: 'Expired' }, + duplicateSuffix: ' copy', + countdown: { overdue: 'Overdue', soon: 'Soon', minutes: (count) => `in ${count} ${count === 1 ? 'minute' : 'minutes'}`, hours: (count) => `in ${count} ${count === 1 ? 'hour' : 'hours'}`, tomorrow: 'Tomorrow', days: (count) => `in ${count} days`, weeks: (count) => `in ${count} ${count === 1 ? 'week' : 'weeks'}`, months: (count) => `in ${count} ${count === 1 ? 'month' : 'months'}` }, + recurrence: { once: 'One-time task', cron: (expression) => `Cron: ${expression}`, recurring: { daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly' }, interval: (seconds) => `Every ${seconds} seconds` }, + runStatus: { ok: 'Triggered', blocked: 'Blocked', failed: 'Failed' }, + delivery: { local: 'Local notification', bot: (provider, chatId) => `${provider} · ${chatId}`, fallback: (target) => `Deliver to: ${target}` }, + form: { + editTitle: 'Edit scheduled task', createTitle: 'New scheduled task', useTemplate: 'Use template', field: { title: 'Title', time: 'Task time', channel: 'Method', recurrence: 'Repeat', platform: 'Platform', cron: 'Cron', chatId: 'Chat ID', note: 'Notes' }, titlePlaceholder: 'For example: Review project progress tomorrow', groupSchedule: 'Frequency', groupDelivery: 'Delivery', presetsAriaLabel: 'Quick task times', presets: [['ten-minutes', 'In 10 minutes'], ['one-hour', 'In 1 hour'], ['tomorrow-morning', 'Tomorrow at 9:00'], ['next-monday', 'Next Monday at 9:00']], recurrenceOptions: [['none', 'Does not repeat'], ['daily', 'Daily'], ['weekly', 'Weekly'], ['monthly', 'Monthly'], ['cron', 'Cron']], deliveryOptions: [['local', 'Local notification'], ['bot', 'Bot chat']], agentRunOption: 'Run via the Agent', intervalOption: 'Fixed interval (created by Agent)', cronPlaceholder: 'For example 0 9 * * 1-5', chatIdPlaceholder: 'For example Telegram chat_id', deliveryHelp: (providers) => `Available delivery providers: ${providers}. Other bot platforms are not shown as delivery targets.`, notePlaceholder: 'Optional context for this task', saving: 'Saving…', creating: 'Creating…', save: 'Save', create: 'Create', + }, + page: { + title: 'Scheduled tasks', refreshing: 'Refreshing scheduled tasks', refresh: 'Refresh scheduled tasks', create: 'New scheduled task', keepAwake: 'Keep system awake', pageSettings: 'Scheduled task page settings', keepAwakeErrorTitle: 'Could not update Keep system awake', keepAwakeErrorFallback: 'Could not update the Keep system awake setting. Try again later.', viewsAriaLabel: 'Scheduled task views', tasks: 'My scheduled tasks', runs: 'Run history', filtersAriaLabel: 'Scheduled task filters', sort: 'Sort', sortOptions: [['created-desc', 'Newest created first'], ['next-run-asc', 'Next run first'], ['updated-desc', 'Recently updated first']], searchLabel: 'Search scheduled tasks', searchPlaceholder: 'Search titles, notes, delivery, or run history…', state: 'Status', filterOption: (label, count) => `${label} ${count}`, active: 'Active', all: 'All', range: 'Range', rangeOptions: [['day', 'Today'], ['week', 'Last 7 days'], ['month', 'Last 30 days'], ['all', 'All runs']], searchMatches: (count) => `${count} matching ${count === 1 ? 'task' : 'tasks'}`, clearSearch: 'Clear search', noSearchTitle: 'No matching tasks', noFilterTitle: 'No tasks in this filter', noSearchBody: 'Change the search terms or status filter to find other tasks.', noFilterBody: 'Change the filter or create a new scheduled task.', emptyTitle: 'No scheduled tasks yet', emptyBody: 'Create a task so Maka can continue this work at the right time.', listAriaLabel: 'Scheduled task list', inspectorOpened: (title) => `Opened the task details for ${title}`, edit: 'Edit', duplicate: 'Duplicate', triggering: 'Triggering…', triggerNow: 'Trigger now', snoozing: 'Snoozing…', snooze: 'Snooze 10 minutes', clearing: 'Clearing…', clearRuns: 'Clear history', deleting: 'Deleting…', delete: 'Delete', nextRun: (time) => `Next run: ${time}`, recentRun: (time) => `Last run ${time}`, unscheduled: 'Not scheduled', noRunsTitle: 'No run history', noRunsBody: 'Triggered tasks, manual runs, and delivery failures appear here.', showAllTime: 'All time', runsAriaLabel: 'Scheduled task run history', activeCount: (count) => `${count} active`, + }, + detail: { + label: 'Task details', + enabled: 'Enabled', + recurrence: 'Repeats', + nextRun: 'Next run', + lastRun: 'Last run', + delivery: 'Delivery', + created: 'Created', + runs: 'Run history', + noRuns: 'This task has not run yet.', + agentSource: 'Agent scheduled task', + agentSourceHint: + 'When due, Maka starts a new task using the execution settings captured at creation.', + agentDelivery: 'Run via the Agent', + }, + } } satisfies UiCatalog; export function getScheduledTaskCopy(locale: UiLocale): ScheduledTaskCopy { diff --git a/packages/ui/src/session-hover-card-copy.ts b/packages/ui/src/session-hover-card-copy.ts index 0cffea4b73..d1f2c238e0 100644 --- a/packages/ui/src/session-hover-card-copy.ts +++ b/packages/ui/src/session-hover-card-copy.ts @@ -57,6 +57,18 @@ const COPY: Record = { projectAvailable: 'Directory available', projectUnavailable: 'Directory unavailable', }, + ko: { + sessionDetailsLabel: (name) => `${name} 작업 세부 정보`, + projectDetailsLabel: (name) => `${name} 프로젝트 세부 정보`, + groupDetailsLabel: (name) => `${name} 그룹 세부 정보`, + noMessages: '아직 메시지가 없습니다', + updated: '업데이트됨', + taskCount: (count) => `${count}개 작업`, + runningTaskCount: (count) => `${count}개 실행 중`, + locationCount: (count) => `${count}개 위치`, + projectAvailable: '디렉터리 사용 가능', + projectUnavailable: '디렉터리 사용 불가', + }, }; export function getSessionHoverCardCopy(locale: UiLocale): SessionHoverCardCopy { diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 5002ac9414..d6d10f9983 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -288,6 +288,90 @@ const SHARED_UI_COPY = { artifact: { unknownSize: 'Unknown size' }, providers: { minimaxChina: 'MiniMax China', custom: 'Custom', claudeSubscription: 'Claude subscription' }, }, + ko: { + capabilityAudit: { + ariaLabel: 'Capability risks', + needsAuthorization: (count) => `${count} ${count === 1 ? 'source' : 'sources'} awaiting authorization`, + sourceErrors: (count) => `${count} ${count === 1 ? 'source has' : 'sources have'} errors`, + failedScheduledTasks: (count) => `${count} scheduled ${count === 1 ? 'task failed' : 'tasks failed'} last run`, + skippedScheduledTasks: (count) => `${count} scheduled ${count === 1 ? 'task was' : 'tasks were'} skipped last run`, + }, + markdown: { + invalidInternalLink: 'Invalid internal link', + unsafeLink: 'Unsafe link', + taskList: 'Task list', + table: 'Table', + checkbox: 'Checkbox', + code: 'Code', + opensInNewTab: '(opens in new tab)', + copyCode: 'Copy code', + copiedCode: 'Code copied', + mermaidDiagram: 'Mermaid diagram', + mermaidRendering: 'Rendering Mermaid diagram…', + mermaidRenderFailed: 'Could not render the Mermaid diagram. Showing source.', + mermaidTooLarge: 'Mermaid diagram source is too large. Showing source.', + mermaidDeferred: 'This diagram was not rendered automatically to limit resource usage.', + mermaidRender: 'Render diagram', + mermaidViewSource: 'View Mermaid source', + mermaidToolbar: 'Mermaid diagram toolbar', + mermaidViewport: 'Mermaid diagram viewport. Drag to pan; press plus or minus to zoom.', + mermaidZoomIn: 'Zoom in on diagram', + mermaidZoomOut: 'Zoom out on diagram', + mermaidResetView: 'Fit diagram to viewport', + mermaidExpandView: 'View diagram fullscreen', + mermaidCollapseView: 'Exit diagram fullscreen', + mermaidZoomLevel: (percent) => `Zoom level ${percent}%`, + }, + formControls: { + selectPlaceholder: 'Select…', + clear: 'Clear {label}', + required: 'Required', + optional: 'Optional', + }, + modelPicker: { + searchPlaceholder: 'Search models…', + knowledgeCutoff: (date) => `Knowledge cutoff: ${date}`, + }, + moduleHubs: { + extensions: { + title: 'Extensions', + description: 'Manage the skills and external tools Maka can use.', + selectorLabel: (module) => `Extension content: ${module}`, + skills: 'Skills', + mcp: 'MCP', + }, + automations: { + title: 'Scheduled tasks', + description: 'Schedule recurring runs and review progress across local tasks.', + selectorLabel: (module) => `Scheduled task content: ${module}`, + scheduledTasks: 'Scheduled tasks', + dailyReview: 'Daily review', + }, + }, + modules: { + skills: 'Skills', + loadingSkills: 'Loading skills…', + automations: 'Scheduled tasks', + loadingAutomations: 'Loading scheduled tasks…', + dailyReview: 'Daily review', + loadingDailyReview: 'Loading daily review…', + dailyReviewDescription: 'Summarize local tasks into highlights, missed items, and deeper analysis. Scheduled runs can be enabled in Settings.', + dailyReviewDisconnectedTitle: 'Waiting for daily review data', + dailyReviewDisconnectedBody: 'The desktop data bridge is not connected.', + }, + primitives: { loading: 'Loading', close: 'Close', resizeHandle: 'Resize handle' }, + sessionTodo: { + ariaLabel: 'To-do list', + retry: 'Reload the to-do list', + loading: 'Loading the to-do list…', + activeAriaLabel: 'In-progress to-dos', + empty: 'This task has no to-dos yet', + }, + toast: { notifications: 'Notifications', closeNotification: 'Close notification', confirm: 'Confirm', cancel: 'Cancel' }, + stream: { assistantChunkTruncated: '\n[…single delta truncated]\n', assistantTailTruncated: '\n\n[…remaining output truncated]', thinkingHeadTruncated: '[…earlier reasoning truncated]\n', thinkingChunkTruncated: '\n[…single delta truncated]\n', toolChunkTruncated: '\n[…truncated]\n' }, + artifact: { unknownSize: 'Unknown size' }, + providers: { minimaxChina: 'MiniMax China', custom: 'Custom', claudeSubscription: 'Claude subscription' }, + } } satisfies UiCatalog; export function getSharedUiCopy(locale: UiLocale): SharedUiCopy { diff --git a/packages/ui/src/shell-controls-copy.ts b/packages/ui/src/shell-controls-copy.ts index c1d184544a..efd087c9dd 100644 --- a/packages/ui/src/shell-controls-copy.ts +++ b/packages/ui/src/shell-controls-copy.ts @@ -117,6 +117,38 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { resultsLabel: 'Search results', }, }, + ko: { + shared: { close: 'Close' }, + navigation: { + mainLabel: 'Main navigation', + newTask: 'New task', + automations: 'Scheduled tasks', + extensions: 'Extensions', + settings: 'Settings', + updateDownloaded: (version: string) => `Update ${version} downloaded. Restart to install.`, + updateFailed: (version: string) => `Update ${version} failed. Click to retry or download manually.`, + pendingTasks: (count: number) => `Scheduled tasks, ${count} active`, + }, + search: { + title: 'Search', + conversationsLabel: 'Search tasks', + placeholder: 'Search task titles and content…', + clearLabel: 'Clear search', + statusRegionLabel: 'Search status and results', + unavailable: 'Search is unavailable in the current environment. Try again later.', + privacyTitle: 'Search is disabled in privacy mode.', + privacyDetail: 'Turn off privacy mode to search previous tasks by keyword.', + errorTitle: 'Search could not be completed.', + errorFallback: 'Search needs to be refreshed. Try again.', + introduction: + 'Start typing to search previous tasks by keyword. Results include local task titles and content only and are not sent over the network.', + searching: 'Searching…', + empty: 'No matching task titles or content. Try another keyword.', + results: (count: number) => `${count} ${count === 1 ? 'match' : 'matches'}`, + truncatedResults: (count: number) => `Many results; showing the first ${count}`, + resultsLabel: 'Search results', + }, + } } satisfies UiCatalog; export function getShellControlsCopy(locale: UiLocale): ShellControlsCopy { diff --git a/packages/ui/src/skills-copy.ts b/packages/ui/src/skills-copy.ts index 5f2f569563..63c4e9a63a 100644 --- a/packages/ui/src/skills-copy.ts +++ b/packages/ui/src/skills-copy.ts @@ -189,6 +189,21 @@ const SKILLS_COPY = { page: { title: 'Skills', toolbarAria: 'Skill filters and views', metaInstalled: (count) => `${count} installed`, metaUpdates: (count) => count === 1 ? '1 update available' : `${count} updates available`, metaAvailable: (count) => count === 1 ? '1 available to install' : `${count} available to install`, searchMatches: (count) => `${count} ${count === 1 ? 'match' : 'matches'}`, search: 'Search skills', openFolder: 'Open folder', moreActions: 'More Skill actions', refreshing: 'Refreshing…', refresh: 'Refresh' }, detail: { label: 'Skill details', enabled: 'Enabled', pinned: 'Pinned', inspectorOpened: (name) => `${name} details opened`, idLabel: 'ID', scopeLabel: 'Scope', sourceLabel: 'Source', contextLabel: 'Context', runtimeLabel: 'Runtime', toolsLabel: 'Declared tools', pathLabel: 'Path' }, }, + ko: { + categories: { '内容创作': 'Content creation', '数据与AI': 'Data & AI', '设计与UI': 'Design & UI', 'DevOps与部署': 'DevOps & deployment', '文档与写作': 'Documents & writing', '效率工具': 'Productivity', '研究与分析': 'Research & analysis' }, + market: { categoryAll: 'All categories', sortName: 'Sort: Name', sortRecent: 'Sort: Recent', controls: 'Marketplace filters and sorting', categoryFilter: 'Filter marketplace skills by category', sortAriaLabel: 'Marketplace skill sort order', ariaLabel: 'Skill marketplace', importLocal: 'Import local Skill', emptySearchTitle: 'No matching marketplace skills', emptyTitle: 'The source library is empty', emptySearchBody: 'Try another keyword or clear search to see all sources.', emptyBody: 'Import a local file containing SKILL.md to make it available as an installable source.', emptyFilterBody: 'Try another category or keyword, or clear the filters.', clearSearch: 'Clear search', clearFilters: 'Clear filters', sourceFallback: 'Local source-library Skill.' }, + tabs: { ariaLabel: 'Skill views', market: 'Marketplace', builtin: 'Built in', installed: 'Installed' }, + install: { action: (name) => `Install ${name}`, installedAction: (name) => `${name} is installed in this workspace`, installedTitle: 'Installed in this workspace', installed: 'Installed', notInstalled: 'Not installed' }, + builtin: { ariaLabel: 'Built-in skills', emptyTitle: 'No built-in skills', emptyBody: 'Skills included with the app appear here.', noMatchTitle: 'No matching built-in skills', noMatchBody: 'Try another keyword or clear search to see all built-in skills.', fallback: 'Skill included with the app.', toolCount: (count: number) => (count === 1 ? '1 tool' : `${count} tools`) }, + installed: { emptySearchTitle: 'No matching Skills', emptyTitle: 'Waiting for a Skill', emptySearchBody: 'Try another keyword or clear search to see all local skills.', emptyBodyBeforeCode: 'Place a folder containing', emptyBodyAfterCode: 'in the workspace skills/ directory, then refresh to show it here.', refreshPending: 'Refreshing…', refresh: 'Refresh skills', listAriaLabel: 'Skill list' }, + context: { scope: { project: 'Project', workspace: 'Workspace', user: 'User', custom: 'Custom' }, decision: { advertised: 'In context', disabled: 'Disabled', invalid: 'Invalid metadata', host_incompatible: 'Host incompatible', shadowed: 'Shadowed', budget: 'Budget omitted' }, needsReview: 'Needs review', discoverySource: (scope, source) => `${scope}/${source} discovery source`, discoveryDiagnostic: { blocked_path: 'Path blocked by the safety policy', read_failed: 'Source could not be read' } }, + row: { opening: 'Opening…', reviewing: 'Reviewing…', use: 'Use', openTitle: 'Open SKILL.md', pinTitle: 'Pin to the skill context', unpinTitle: 'Unpin', viewDiff: 'View diff', viewUpdate: 'View update', confirmDeleteAriaLabel: (name) => `Delete ${name}?`, deleteDescription: 'This removes the Skill files and cannot be undone.', cancel: 'Cancel', delete: 'Delete' }, + review: { ariaLabel: 'Skill update review', title: 'Update review', source: (id) => `Source ${id}`, managedSource: 'Managed source', hasBaseline: 'Baseline available', missingBaseline: 'No baseline', lineTransition: (current, source) => `${current} → ${source} lines`, changedLines: (count) => `${count} ${count === 1 ? 'line differs' : 'lines differ'}`, warning: 'The workspace copy has local changes. Continuing will replace the current SKILL.md with the source version.', workspace: 'Current workspace', sourceVersion: 'Source version', cancel: 'Cancel', overwrite: 'Overwrite local changes', update: 'Update to source version' }, + description: { document: 'Create, edit, and inspect documents.', presentation: 'Create, edit, and inspect presentations.', spreadsheet: 'Create, edit, and analyze spreadsheet data.', image: 'Generate or edit images.', browser: 'Open, inspect, and operate web interfaces.', macos: 'Build and debug macOS apps.', fallback: 'Open the skill file to see when to use it.' }, + status: { metadataError: 'Metadata error', managed: { source_missing: 'Source missing', update_available: 'Update available', local_modified: 'Locally modified', metadata_error: 'Metadata error', up_to_date: 'Managed', not_managed: 'Managed' }, modified: 'Modified', bundled: 'Built in', local: 'Local', stateError: 'State error', enabled: 'Enabled', disabled: 'Disabled' }, + page: { title: 'Skills', toolbarAria: 'Skill filters and views', metaInstalled: (count) => `${count} installed`, metaUpdates: (count) => count === 1 ? '1 update available' : `${count} updates available`, metaAvailable: (count) => count === 1 ? '1 available to install' : `${count} available to install`, searchMatches: (count) => `${count} ${count === 1 ? 'match' : 'matches'}`, search: 'Search skills', openFolder: 'Open folder', moreActions: 'More Skill actions', refreshing: 'Refreshing…', refresh: 'Refresh' }, + detail: { label: 'Skill details', enabled: 'Enabled', pinned: 'Pinned', inspectorOpened: (name) => `${name} details opened`, idLabel: 'ID', scopeLabel: 'Scope', sourceLabel: 'Source', contextLabel: 'Context', runtimeLabel: 'Runtime', toolsLabel: 'Declared tools', pathLabel: 'Path' }, + } } satisfies UiCatalog; export function getSkillsCopy(locale: UiLocale): SkillsCopy { diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts index bc1fb04be6..cfde084f13 100644 --- a/packages/ui/src/tool-activity/copy.ts +++ b/packages/ui/src/tool-activity/copy.ts @@ -383,6 +383,107 @@ const TOOL_ACTIVITY_COPY = { readOnly: 'Read only', }, }, + ko: { + errorLabel: 'Error', + status: { sandboxBlocked: 'Possibly blocked by sandbox', interrupted: 'Interrupted' }, + output: { redacted: '[Redacted]', truncated: 'Output truncated' }, + copy: { + idle: 'Copy', + pending: 'Copying…', + copied: 'Copied', + failed: 'Copy failed', + actionAriaLabel: (action, identity) => `${action}: ${identity}`, + }, + sandboxBlocked: { title: 'Operation may have been blocked by sandbox', description: 'The sandbox may have blocked at least one action in this call. Some effects may have occurred before it failed; check the output and workspace state before retrying.', copyAriaLabel: (label) => `${label} sandbox diagnostics` }, + requiresBypass: { + title: 'Bypass mode required', + description: 'This action controls a local app directly and cannot run inside the sandbox.', + action: 'Switch and retry', + pending: 'Switching…', + }, + computer: { + fallback: 'Use the computer', + listApps: 'List open apps', + launchApp: (app) => `Open “${app}”`, + launchAppUnknown: 'Open an app', + observe: 'Observe the current window', + observeApp: (app) => `Observe the “${app}” window`, + observeWindow: (windowId) => `Observe window ${windowId}`, + observeMenu: (menu) => `Inspect the “${menu}” menu`, + screenshot: 'Screenshot the current window', + screenshotApp: (app) => `Screenshot the “${app}” window`, + screenshotWindow: (windowId) => `Screenshot window ${windowId}`, + element: (elementId) => `element ${elementId}`, + elementUnknown: 'the element', + clickElement: (element) => `Click ${element}`, + setValue: (element) => `Set the value of ${element}`, + selectText: (element) => `Select text in ${element}`, + secondaryAction: (element) => `Run a secondary action on ${element}`, + scrollElement: (element) => `Scroll ${element}`, + elementSequence: (count) => `Operate ${count} controls`, + elementSequenceUnknown: 'Operate multiple controls', + windowAction: 'Operate the window', + windowMove: 'Move the window', + windowResize: 'Resize the window', + windowMinimize: 'Minimize the window', + targetApp: (app) => `“${app}” window`, + targetWindow: (windowId) => `Window ${windowId}`, + runningAction: (action, target) => target ? `${action} · ${target}` : action, + runningSequence: (current, total, target) => + target + ? `Operating ${target} · step ${current}/${total}` + : `Operating controls · step ${current}/${total}`, + scroll: 'Scroll', + pressKey: 'Press a key', + type: 'Type text', + holdKey: 'Hold a key', + wait: 'Wait', + zoom: 'Zoom into a region', + cursorPosition: 'Read the pointer position', + pointer: { + move: 'Move the pointer', + left: 'Click', + right: 'Right-click', + middle: 'Middle-click', + double: 'Double-click', + triple: 'Triple-click', + down: 'Press the mouse', + up: 'Release the mouse', + drag: 'Drag', + }, + }, + loadTools: { + displayName: 'Enable capabilities', + genericAction: 'Enable tool capabilities', + genericTitle: 'Tool capabilities enabled', + genericDescription: 'This tool group is ready to use.', + count: (n) => `${n} ${n === 1 ? 'capability' : 'capabilities'} available`, + technicalDetails: 'Technical details', + groupId: 'Group', + toolIds: 'Tools', + groups: { + browser: { label: 'Browser', action: 'Enable browser actions', title: 'Browser actions enabled', description: 'Open pages, read content, and interact with websites.' }, + computer_use: { label: 'Computer Use', action: 'Enable desktop actions', title: 'Desktop actions enabled', description: 'View and operate authorized local applications.' }, + mcp: { label: 'MCP', action: 'Connect MCP', title: 'MCP tools connected', description: 'Use MCP services connected by the current client.' }, + rive: { label: 'Rive', action: 'Enable Rive workflows', title: 'Rive workflows enabled', description: 'Run durable multi-agent workflows.' }, + agent: { label: 'Agent', action: 'Enable subagents', title: 'Subagent collaboration enabled', description: 'Delegate, track, and summarize tasks in parallel.' }, + settings: { label: 'Settings', action: 'Enable settings tools', title: 'Settings tools enabled', description: 'Read or update settings owned by the current client.' }, + }, + }, + permissionDenied: 'User denied the permission request', + result: { + hiddenLines: (n) => `… ${n} ${n === 1 ? 'line' : 'lines'} hidden`, ptyFailed: 'Background terminal interaction failed', queued: 'Entered', notQueued: 'Not entered', queuedPreview: (action, preview, bytes) => bytes === undefined ? `${action}: ${preview}` : `${action}: ${preview}… · ${bytes} bytes total`, byteCount: (action, bytes) => `${action} ${bytes} bytes`, resizeNotApplied: (size) => `Not resized to ${size}`, resized: (size) => `Resized to ${size}`, sizeUnchanged: (size) => `Size already ${size}`, ptyCompleted: 'Background terminal interaction completed', terminalUnavailable: 'Terminal output unavailable', noTerminalFrame: '(No terminal frame available)', noOutputYet: '(No output yet)', noOutput: '(No output)', exitCode: (code) => `exit code ${code}`, managedBySource: 'Managed by the source task', sourceUnavailable: 'Source task unavailable', running: 'Running', success: 'Succeeded', failed: 'Failed', timedOut: 'Timed out', cancelled: 'Cancelled', disconnected: 'Disconnected', terminalTruncated: 'Terminal output truncated', terminalRedacted: 'Terminal output redacted', streamHidden: (stream, n) => `… ${n} ${stream} ${n === 1 ? 'line' : 'lines'} hidden`, streamsTruncated: (limit) => `Output truncated · showing the first ${limit} lines of each stream`, outputTruncated: 'Output truncated', outputRedacted: 'Output redacted', + backgroundStatus: { running: 'Running in background', completed: 'Background task completed', failed: 'Background task failed', timed_out: 'Background task timed out', cancelled: 'Background task cancelled', orphaned: 'Background task disconnected' }, backgroundUnknown: (status) => `Background · ${status}`, + workflow: { action: 'Action', status: 'Status', error: 'Error', nodes: 'Node summary', diagnostics: 'Diagnostic excerpts' }, webNoResults: 'No results', webResults: (n) => `${n} ${n === 1 ? 'result' : 'results'}`, credentialSource: { env: 'Environment variable', settings: 'Locally saved key', missing: 'Not configured', unknown: 'Unknown source' }, webFailure: 'Search failed', webSearch: 'Web search', webGuidance: { env: 'Check TAVILY_API_KEY / MAKA_TAVILY_API_KEY and restart.', settings: 'Update the Tavily key in Settings · Web search.', rate_limited: 'Tavily is rate-limiting requests. Try again later or use another credential.', not_configured: 'Configure web search before retrying.', timed_out: 'The request timed out. Try again later.', privacy_mode: 'Web search is disabled in privacy mode.', unknown: 'Check the network connection or try again later.' }, + workflowCompleted: 'Rive workflow completed', + workflowFailed: 'Rive workflow failed', + fileWritten: (bytes, path) => `Wrote ${bytes} bytes to ${path}`, + }, + agent: { + subagentStatus: { completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled', running: 'Running', waiting_for_user: 'Waiting for user input' }, + readOnly: 'Read only', + }, + } } satisfies UiCatalog; export function getToolActivityCopy(locale: UiLocale): ToolActivityCopy {