From 7b45e3244565e1c6d34cfcd75fab88c0dbf42bed Mon Sep 17 00:00:00 2001 From: heeeione Date: Sun, 6 Sep 2026 15:27:54 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(i18n-ko):=20core=20=E2=80=94=20add=20k?= =?UTF-8?q?o=20to=20UiLocale=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `ko` to UI_LOCALES so the closed locale vocabulary carries Korean: the `isUiLocale`/`isUiLocalePreference` guards accept it, and `resolveSystemUiLocale` recognizes the `ko` prefix through the existing case-insensitive, `_`-normalizing path (`ko`, `ko-KR`, `ko_KR`, `ko_KR.UTF-8`), so an `auto` preference resolves to it without being persisted. `uiLocaleToIntlLocale` stops being identity. Every locale so far was already the tag `Intl` wants; bare `ko` leaves the region open, and the region is what selects Korean date, number, and plural formatting, so it is widened to `ko-KR`. The return type becomes the literal union of the tags actually emitted — `'zh-CN' | 'zh-TW' | 'en' | 'ko-KR'` — rather than `string`, so the set stays visible in the signature. Every call site feeds an `Intl` constructor, `toLocaleString`, or `localeCompare`, all of which take `string`, so narrowing it is safe. The formatter test can no longer assert identity, so it pins the tag table instead and checks each tag is canonical, which keeps a locale added later from reaching `Intl` without a deliberate tag. Refs #3975 Generated-by: Claude Code --- packages/core/src/__tests__/ui-locale.test.ts | 51 +++++++++++++++++-- packages/core/src/ui-locale.ts | 19 +++++-- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/packages/core/src/__tests__/ui-locale.test.ts b/packages/core/src/__tests__/ui-locale.test.ts index 3304657925..cccfa3bf7e 100644 --- a/packages/core/src/__tests__/ui-locale.test.ts +++ b/packages/core/src/__tests__/ui-locale.test.ts @@ -34,14 +34,18 @@ import { describe('UI locale', () => { it('accepts only the supported resolved locales and preferences', () => { - assert.equal(['zh-CN', 'zh-TW', 'en'].every(isUiLocale), true); + assert.equal(['zh-CN', 'zh-TW', 'ko', 'en'].every(isUiLocale), true); assert.equal(isUiLocale('zh'), false); - assert.equal(['auto', 'zh-CN', 'zh-TW', 'en'].every(isUiLocalePreference), true); + assert.equal(isUiLocale('ko-KR'), false); + assert.equal(['auto', 'zh-CN', 'zh-TW', 'ko', 'en'].every(isUiLocalePreference), true); }); it('normalizes the legacy persisted preference without widening the locale contract', () => { assert.equal(normalizeUiLocalePreference('zh'), 'zh-CN'); assert.equal(normalizeUiLocalePreference('zh-TW'), 'zh-TW'); + assert.equal(normalizeUiLocalePreference('ko'), 'ko'); + assert.equal(normalizeUiLocalePreference('auto'), 'auto'); + assert.equal(normalizeUiLocalePreference('ko-KR'), 'auto'); assert.equal(normalizeUiLocalePreference('unsupported'), 'auto'); }); @@ -54,6 +58,17 @@ describe('UI locale', () => { [['zh-HK'], 'zh-TW'], [['zh_MO'], 'zh-TW'], [['zh_TW.UTF-8'], 'zh-TW'], + [['ko'], 'ko'], + [['ko-KR'], 'ko'], + [['ko_KR'], 'ko'], + [['KO-Kr'], 'ko'], + [['ko_KR.UTF-8'], 'ko'], + [['ko-Kore-KR'], 'ko'], + [['ko', 'ko-KR', 'en'], 'ko'], + [['en', 'ko'], 'en'], + [['ko', 'en'], 'ko'], + [['fr-FR', 'ko-KR'], 'ko'], + [['kok-IN'], 'en'], [['fr-FR', 'en-US'], 'en'], [[], 'en'], ] as const) { @@ -64,18 +79,41 @@ describe('UI locale', () => { it('resolves explicit preferences and overrides before the system locale', () => { assert.equal(resolveUiLocale('auto', 'zh-TW'), 'zh-TW'); + assert.equal(resolveUiLocale('auto', 'ko'), 'ko'); assert.equal(resolveUiLocale('zh-CN', 'zh-TW'), 'zh-CN'); + assert.equal(resolveUiLocale('ko', 'en'), 'ko'); + assert.equal(resolveUiLocale('en', 'ko'), 'en'); assert.equal(resolveUiLocale('zh-CN', 'zh-CN', 'en'), 'en'); + assert.equal(resolveUiLocale('auto', 'ko', 'zh-TW'), 'zh-TW'); }); it('keeps every locale guard and formatter in step with UI_LOCALES', () => { + // `ko` is the first locale whose Intl tag is not its own name, so this can + // no longer assert identity. Pinning the table keeps a locale added later + // from silently reaching `Intl` without a deliberate tag. + const intlTags: Record<(typeof UI_LOCALES)[number], string> = { + 'zh-CN': 'zh-CN', + 'zh-TW': 'zh-TW', + ko: 'ko-KR', + en: 'en', + }; for (const locale of UI_LOCALES) { assert.ok(isUiLocale(locale), locale); assert.equal(resolveSystemUiLocale([locale]), locale); - assert.equal(uiLocaleToIntlLocale(locale), locale); + assert.equal(uiLocaleToIntlLocale(locale), intlTags[locale]); } const intlLocales = UI_LOCALES.map(uiLocaleToIntlLocale); assert.equal(new Set(intlLocales).size, UI_LOCALES.length); + for (const tag of intlLocales) { + assert.equal(new Intl.Locale(tag).baseName, tag, tag); + } + }); + + it('maps ko onto the region-qualified Intl tag', () => { + assert.equal(uiLocaleToIntlLocale('ko'), 'ko-KR'); + assert.equal(uiLocaleToIntlLocale('en'), 'en'); + assert.equal(uiLocaleToIntlLocale('zh-CN'), 'zh-CN'); + assert.equal(uiLocaleToIntlLocale('zh-TW'), 'zh-TW'); }); }); @@ -87,12 +125,14 @@ describe('UI message catalogs', () => { }>()({ en: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, 'zh-CN': { title: '状态', detail: { ready: '就绪' } }, + ko: { title: '상태', detail: { ready: '준비됨' } }, }); assert.deepEqual(resolveUiMessageCatalog(catalog), { en: { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, 'zh-CN': { title: '状态', detail: { ready: '就绪', waiting: 'Waiting' } }, 'zh-TW': { title: 'Status', detail: { ready: 'Ready', waiting: 'Waiting' } }, + ko: { title: '상태', detail: { ready: '준비됨', waiting: 'Waiting' } }, }); }); @@ -101,6 +141,11 @@ describe('UI message catalogs', () => { assert.equal(formatUiMessage(template, { count: 1 }, 'en'), '1 tool'); assert.equal(formatUiMessage(template, { count: 3 }, 'en'), '3 tools'); + + // Korean has one plural form; both counts take the `other` branch. + const koTemplate = '{count, plural, other {도구 #개}}'; + assert.equal(formatUiMessage(koTemplate, { count: 1 }, 'ko'), '도구 1개'); + assert.equal(formatUiMessage(koTemplate, { count: 3 }, 'ko'), '도구 3개'); }); it('fails soft for missing or inherited interpolation values', () => { diff --git a/packages/core/src/ui-locale.ts b/packages/core/src/ui-locale.ts index 2272506d79..ba3aceaad2 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-CN', 'zh-TW', 'en'] as const; +export const UI_LOCALES = ['zh-CN', 'zh-TW', 'ko', 'en'] as const; export type UiLocale = (typeof UI_LOCALES)[number]; @@ -120,7 +120,7 @@ function isMessageRecord(value: unknown): value is Readonly Date: Wed, 2 Sep 2026 08:10:05 +0900 Subject: [PATCH 2/4] feat(i18n-ko): add Korean locale for native surfaces and E2E (#3980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ko to UI_LOCALES and resolve ko-KR system locales - Localize startup/quit/boot, diagnostics, and notification native copy - Add ko E2E fixture flags and ko-locale spec - Add ko stubs to UiCatalog entries (renderer English fallback until #3977–#3979) Generated-by: Cursor Co-authored-by: Cursor --- apps/desktop/e2e/fixtures.ts | 4 +- apps/desktop/e2e/ko-locale.spec.ts | 46 ++ .../main/__tests__/e2e-fixture-locale.test.ts | 12 +- .../main/client-settings-confirmation-copy.ts | 7 + .../src/main/computer-use/status-item.ts | 5 + apps/desktop/src/main/e2e-fixture.ts | 1 + .../src/main/native-diagnostic-dialog-copy.ts | 39 ++ apps/desktop/src/main/notifications-policy.ts | 4 + .../permission-overlay-copy.ts | 19 + apps/desktop/src/main/project-picker-copy.ts | 2 +- .../src/main/runtime-host-boot-copy.ts | 16 + .../src/main/runtime-host-quit-copy.ts | 10 + .../settings-provider-copy.ts | 1 + .../src/renderer/locales/artifact-copy.ts | 36 ++ .../src/renderer/locales/browser-copy.ts | 24 + .../src/renderer/locales/conversation-copy.ts | 236 +++++++ .../locales/external-session-import-copy.ts | 55 ++ apps/desktop/src/renderer/locales/mcp-copy.ts | 59 ++ .../src/renderer/locales/onboarding-copy.ts | 3 + .../locales/permission-center-copy.ts | 47 ++ .../src/renderer/locales/plan-mode-copy.ts | 20 + .../locales/session-collaboration-copy.ts | 2 +- .../src/renderer/locales/settings-bot-copy.ts | 1 + .../locales/settings-daily-review-copy.ts | 19 + .../renderer/locales/settings-data-copy.ts | 27 + .../renderer/locales/settings-health-copy.ts | 19 + .../renderer/locales/settings-memory-copy.ts | 47 ++ .../locales/settings-navigation-copy.ts | 26 + .../locales/settings-preferences-copy.ts | 53 ++ .../locales/settings-projects-copy.ts | 311 +++++++++ .../renderer/locales/settings-shared-copy.ts | 42 ++ .../locales/settings-subagents-copy.ts | 82 +++ .../renderer/locales/settings-tasks-copy.ts | 35 ++ .../locales/settings-test-result-copy.ts | 30 + .../renderer/locales/settings-usage-copy.ts | 22 + .../locales/settings-web-search-copy.ts | 17 + .../src/renderer/locales/shell-copy.ts | 591 ++++++++++++++++++ .../renderer/locales/shell-remaining-copy.ts | 1 + .../settings/provider-display-copy.ts | 12 +- .../src/__tests__/tui-copy-catalog.test.ts | 10 +- packages/core/src/redaction.ts | 7 + packages/core/src/relative-time.ts | 1 + packages/core/src/tool-quiet-preview.ts | 10 + packages/ui/src/conversation-copy.ts | 158 +++++ packages/ui/src/daily-review-copy.ts | 35 ++ packages/ui/src/locale-helpers.ts | 8 + 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 +++ scripts/add-ko-catalog-stub.mjs | 92 +++ 53 files changed, 2574 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/e2e/ko-locale.spec.ts create mode 100644 scripts/add-ko-catalog-stub.mjs diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 82f06d0436..d30846e117 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -188,7 +188,7 @@ async function seedE2eConnection(userDataDir: string): Promise { } } -async function seedE2eLocale(userDataDir: string, locale: 'zh-CN' | 'zh-TW' | 'en'): Promise { +async function seedE2eLocale(userDataDir: string, locale: 'zh-CN' | 'zh-TW' | 'en' | 'ko'): Promise { const workspaceRoot = path.join(userDataDir, 'workspaces', 'default'); await createSettingsStore(workspaceRoot).update({ personalization: { uiLocale: locale }, @@ -411,7 +411,7 @@ async function withE2eWindow( seed: boolean; readinessSelector: string; e2eFixtureScenario?: string; - locale?: 'zh-CN' | 'zh-TW' | 'en'; + locale?: 'zh-CN' | 'zh-TW' | 'en' | 'ko'; /** #1312: force app:info's platform so the window boots natively into that platform's `data-os` cascade. */ platform?: 'darwin' | 'win32' | 'linux'; /** Show fixtures whose contract depends on compositor-paced frames. */ diff --git a/apps/desktop/e2e/ko-locale.spec.ts b/apps/desktop/e2e/ko-locale.spec.ts new file mode 100644 index 0000000000..1dd75d4ccd --- /dev/null +++ b/apps/desktop/e2e/ko-locale.spec.ts @@ -0,0 +1,46 @@ +/* + * 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 { ensureSidebarExpanded, expect, test } from './fixtures'; + +test('persists Korean locale preference through reload', async ({ window: page }) => { + await ensureSidebarExpanded(page); + await page.getByRole('button', { name: 'Settings', exact: true }).click(); + await expect(page.getByRole('main', { name: 'Settings content' })).toBeVisible(); + await page.getByRole('button', { name: 'General', exact: true }).click(); + await expect(page.getByText('UI language', { exact: true }).first()).toBeVisible(); + await page.keyboard.press('Escape'); + + await page.evaluate(async () => { + await window.maka.settings.update({ personalization: { uiLocale: 'ko' } }); + }); + await page.reload(); + await page.waitForSelector('.maka-composer-editor'); + await ensureSidebarExpanded(page); + + const locale = await page.evaluate(async () => { + const settings = await window.maka.settings.read(); + return settings.personalization.uiLocale; + }); + expect(locale).toBe('ko'); + + await page.getByRole('button', { name: 'Settings', exact: true }).click(); + await page.getByRole('button', { name: 'General', exact: true }).click(); + await expect(page.getByText('UI language', { exact: true }).first()).toBeVisible(); +}); diff --git a/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts b/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts index cec94f10f1..b1b85b2ee7 100644 --- a/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts +++ b/apps/desktop/src/main/__tests__/e2e-fixture-locale.test.ts @@ -21,8 +21,8 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { resolveE2eFixture } from '../e2e-fixture.js'; -test('preserves both canonical Chinese locale fixture flags', () => { - for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) { +test('preserves canonical Chinese and Korean locale fixture flags', () => { + for (const locale of ['zh-CN', 'zh-TW', 'en', 'ko'] as const) { const fixture = resolveE2eFixture( 'settings-general', false, @@ -39,6 +39,14 @@ test('normalizes locale fixture flag casing without widening the contract', () = resolveE2eFixture('settings-general', false, undefined, undefined, 'ZH-tw')?.locale, 'zh-TW', ); + assert.equal( + resolveE2eFixture('settings-general', false, undefined, undefined, 'KO')?.locale, + 'ko', + ); + assert.equal( + resolveE2eFixture('settings-general', false, undefined, undefined, 'ko-kr')?.locale, + 'ko', + ); assert.equal( resolveE2eFixture('settings-general', false, undefined, undefined, 'zh-Hant')?.locale, null, diff --git a/apps/desktop/src/main/client-settings-confirmation-copy.ts b/apps/desktop/src/main/client-settings-confirmation-copy.ts index 57af295461..068a655fd0 100644 --- a/apps/desktop/src/main/client-settings-confirmation-copy.ts +++ b/apps/desktop/src/main/client-settings-confirmation-copy.ts @@ -50,6 +50,13 @@ const COPY = { message: "Allow Maka to update this client's settings?", buttons: ['Apply changes', 'Cancel'], }, + ko: { + labels: { theme: '테마', palette: '팔레트', uiLocale: 'UI 언어', runComplete: '응답 완료 알림', keepSystemAwake: '시스템 깨어 있음 유지' }, + on: '켜짐', + off: '꺼짐', + message: 'Maka가 이 클라이언트 설정을 업데이트하도록 허용할까요?', + buttons: ['변경 적용', '취소'], + }, } satisfies UiCatalog; export function clientSettingsConfirmation( diff --git a/apps/desktop/src/main/computer-use/status-item.ts b/apps/desktop/src/main/computer-use/status-item.ts index 1dba08cb10..4d466ae734 100644 --- a/apps/desktop/src/main/computer-use/status-item.ts +++ b/apps/desktop/src/main/computer-use/status-item.ts @@ -147,6 +147,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/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts index 5be7c0ed42..de67e82b77 100644 --- a/apps/desktop/src/main/e2e-fixture.ts +++ b/apps/desktop/src/main/e2e-fixture.ts @@ -123,6 +123,7 @@ function parseLocaleFlag(raw: string | undefined): UiLocale | null { const normalized = raw?.trim().toLowerCase(); if (normalized === 'zh-cn') return 'zh-CN'; if (normalized === 'zh-tw') return 'zh-TW'; + if (normalized === 'ko' || normalized === 'ko-kr') return 'ko'; return normalized === 'en' ? 'en' : null; } diff --git a/apps/desktop/src/main/native-diagnostic-dialog-copy.ts b/apps/desktop/src/main/native-diagnostic-dialog-copy.ts index 522033559c..5eb438d03d 100644 --- a/apps/desktop/src/main/native-diagnostic-dialog-copy.ts +++ b/apps/desktop/src/main/native-diagnostic-dialog-copy.ts @@ -172,6 +172,45 @@ 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: '종료', + }, + 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/notifications-policy.ts b/apps/desktop/src/main/notifications-policy.ts index 32da6335e4..e7bd5f6579 100644 --- a/apps/desktop/src/main/notifications-policy.ts +++ b/apps/desktop/src/main/notifications-policy.ts @@ -90,6 +90,10 @@ const RUN_NOTIFICATION_COPY = { errored: { title: 'Conversation error', body: 'This response did not finish. Click to view details.' }, completed: { title: 'Response ready', body: 'Maka finished this response. Click to view it.' }, }, + ko: { + errored: { title: '대화 오류', body: '이 응답이 완료되지 않았습니다. 클릭하여 자세히 보세요.' }, + completed: { title: '응답 준비됨', body: 'Maka가 이 응답을 완료했습니다. 클릭하여 확인하세요.' }, + }, } satisfies UiCatalog>; export function runNotificationCopy( 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 5311ff0469..f369c008ff 100644 --- a/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts +++ b/apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts @@ -107,6 +107,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/project-picker-copy.ts b/apps/desktop/src/main/project-picker-copy.ts index 1ce70aa251..a0c1df4424 100644 --- a/apps/desktop/src/main/project-picker-copy.ts +++ b/apps/desktop/src/main/project-picker-copy.ts @@ -19,7 +19,7 @@ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; -const TITLE = { 'zh-CN': '添加项目', 'zh-TW': '新增專案', en: 'Add project' } satisfies UiCatalog; +const TITLE = { 'zh-CN': '添加项目', 'zh-TW': '新增專案', en: 'Add project', ko: '프로젝트 추가' } satisfies UiCatalog; export function projectPickerTitle(locale: UiLocale): string { return TITLE[locale]; diff --git a/apps/desktop/src/main/runtime-host-boot-copy.ts b/apps/desktop/src/main/runtime-host-boot-copy.ts index fb1489d8c7..d60474c84c 100644 --- a/apps/desktop/src/main/runtime-host-boot-copy.ts +++ b/apps/desktop/src/main/runtime-host-boot-copy.ts @@ -83,6 +83,22 @@ const STARTUP_RECOVERY_COPY = { buttons: ['Retry', 'Use Local', 'Keep Offline'], }, }, + ko: { + storageRoot: { + title: 'Maka 작업 공간을 복구해야 합니다', + message: 'Maka가 이 작업 공간을 확인할 수 없습니다.', + detail: (workspaceRoot) => + `시스템의 디스크 식별자가 변경되었을 수 있습니다. 원본 Maka 작업 공간인 경우에만 복구하세요. 복사된 작업 공간이면 복구하지 마세요.\n\n${workspaceRoot}`, + buttons: ['작업 공간 복구', '종료'], + }, + runtimeHost: { + title: '기본 Runtime Host에 연결할 수 없습니다', + message: (profileName) => `${profileName}에 연결할 수 없습니다`, + detail: (message) => + `${message}\n\n다시 시도하거나 Local을 기본 Host로 사용하거나, 현재 선택을 유지한 뒤 나중에 설정에서 처리할 수 있습니다.`, + buttons: ['다시 시도', 'Local 사용', '오프라인 유지'], + }, + }, } satisfies UiCatalog; export function getStartupRecoveryCopy(locale: UiLocale): StartupRecoveryCopy { diff --git a/apps/desktop/src/main/runtime-host-quit-copy.ts b/apps/desktop/src/main/runtime-host-quit-copy.ts index 9b316e77a6..cc03971e10 100644 --- a/apps/desktop/src/main/runtime-host-quit-copy.ts +++ b/apps/desktop/src/main/runtime-host-quit-copy.ts @@ -71,4 +71,14 @@ const COPY = { stopAndQuit: '停止工作並結束', keepRunning: '繼續執行 Maka', }, + 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/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index 13b6193d02..5ad583f05f 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 @@ -647,6 +647,7 @@ const PROVIDER_SETTINGS_COPY = { 'zh-CN': zhCopy, 'zh-TW': zhTwCopy, en: enCopy, + ko: enCopy, } satisfies UiCatalog; export function getProviderSettingsCopy(locale: UiLocale): ProviderSettingsCopy { diff --git a/apps/desktop/src/renderer/locales/artifact-copy.ts b/apps/desktop/src/renderer/locales/artifact-copy.ts index 7c63bb5575..0f6a1a940a 100644 --- a/apps/desktop/src/renderer/locales/artifact-copy.ts +++ b/apps/desktop/src/renderer/locales/artifact-copy.ts @@ -194,6 +194,42 @@ 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: 'Failed to refresh generated files', openFailed: 'Could not show generated file in Finder', copyFailed: 'Copy failed', + readTextFailed: 'Could not read the generated file as text.', copied: 'Generated file text copied', saved: 'Generated file saved as', saveFailed: 'Save as failed', + fallbackName: 'generated file', deleteTitle: (name) => `Delete "${name}"`, deleteDescription: 'Soft delete: mark this record as deleted and keep the file recoverable for 6 hours.', + delete: 'Delete', deleteReadOnly: 'Delete (read-only file)', cancel: 'Cancel', deleted: (name) => `Deleted ${name}`, deleteFailed: (name) => `Failed to delete ${name}`, panelAria: 'Generated file preview panel', + listLoadFailed: 'Failed to load generated files', retrying: 'Retrying…', retry: 'Retry', listAria: 'Generated files', deletedBadge: 'Deleted', + previewNamed: (name) => `Preview ${name}`, empty: 'No generated files', emptyHint: 'Files generated by the assistant appear here.', + back: 'Back to generated files', moreActions: (name) => `More actions for ${name}`, + openInFinder: 'Show in Finder', saveAs: 'Save as', copy: 'Copy', + saveFailures: { not_found: 'The generated file does not exist.', not_allowed: 'The generated file failed the path safety check.', deleted: 'Deleted generated files cannot be saved.', write_failed: 'The destination is not writable.', default: 'Could not save the generated file.' }, + actionFailed: 'The generated file action failed. Try again later.', + }, + preview: { + loadingFile: 'Loading file preview…', loadingDiff: 'Loading diff preview…', loadingHtml: 'Loading HTML preview…', + externalLinks: (count) => `External links are disabled in this preview · ${count} ${count === 1 ? 'link' : 'links'}`, frameTitle: (name) => `Generated file preview · ${name}`, + loadingPdf: 'Loading PDF preview…', pdfFallback: 'If your browser has no built-in PDF viewer, use “Show in Finder” in the More menu.', + rendered: 'Preview', source: 'Source', previewLimited: (limit) => `Showing the first ${limit}. Use the More menu to open or save the complete file.`, + renderLimited: (limit, lines) => `To stay responsive, rich preview is limited to the first ${limit} and ${lines} lines. The complete source remains available.`, + highlightLimited: (limit, lines) => `To stay responsive, syntax highlighting is limited to the first ${limit} and ${lines} lines; the rest is plain text.`, + diffLinesLimited: (count) => `${count} more lines are hidden to keep the preview responsive.`, + readFailed: { title: 'Could not read generated file', description: 'The file may have been deleted externally. Use “Show in Finder” in the More menu to check its location.' }, + notAllowed: { title: 'Could not read generated file', description: 'The path safety check failed because the file is no longer inside the allowed generated-files directory.' }, + tooLarge: (bytes) => ({ title: 'File exceeds preview size', description: `${bytes} bytes exceeds the text preview limit. Use the More menu to open or save the complete file.` }), + deleted: { title: 'This generated file was deleted', description: 'The preview has stopped. Use “Show in Finder” to inspect the original file.' }, + unsupportedMime: { title: 'Unsupported file type', description: 'This generated file’s MIME type is not allowed for inline preview. Use “Show in Finder” or “Save as”.' }, + }, + registry: { + kindDisallowed: { title: 'This type cannot be previewed here', description: 'This generated file cannot be previewed in the panel. Use “Show in Finder”.' }, + mimeDisallowed: { title: 'Preview format not supported', description: 'The MIME type was recognized, but previews currently support only PNG / JPEG / GIF / WebP / AVIF.' }, + unknownType: { title: 'Could not identify file type', description: 'The file has no MIME metadata and its extension did not match. Use “Show in Finder”.' }, + oversize: { title: 'File too large to preview', description: 'Files over 2 MB are not expanded here to avoid loading large images into memory.' }, + readFailed: { title: 'Failed to load preview', description: 'The file could not be read. It may have been deleted, moved, or blocked by permissions. Use “Show in Finder” to inspect it.' }, + unsupported: 'Unsupported preview', name: 'Name', unnamed: '(unnamed)', type: 'Type', size: 'Size', openInFinder: 'Show in Finder', loadingImage: 'Loading image preview…', + }, + } } 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 f2e7a82dc5..3aa396329c 100644 --- a/apps/desktop/src/renderer/locales/browser-copy.ts +++ b/apps/desktop/src/renderer/locales/browser-copy.ts @@ -117,6 +117,30 @@ const BROWSER_COPY = { title: 'Embedded browser', description: 'Enter an address, or ask the assistant to navigate and interact with a page.', }, + ko: { + unsupportedScheme: 'The embedded browser only supports HTTP and HTTPS addresses.', + invalidUrl: 'This address is not valid. Check it and try again.', + openFailed: 'Could not open address', + navigationFailed: 'Browser navigation failed', + navigationFailedDetail: 'The page could not be opened. Try again later.', + panelAria: 'Embedded browser', + panelAriaWithTitle: (title) => `Embedded browser: ${title}`, + insecure: 'This site is served over HTTP, so the connection is not encrypted.', + backAria: 'Go back in browser', + back: 'Back', + forwardAria: 'Go forward in browser', + forward: 'Forward', + stopAria: 'Stop loading page', + refreshAria: 'Reload page', + stop: 'Stop', + refresh: 'Reload', + addressAria: 'Browser address', + addressPlaceholder: 'Enter an address and press Enter', + closeAria: 'Close browser page', + close: 'Close page', + title: 'Embedded browser', + description: 'Enter an address, or ask the assistant to navigate and interact with a page.', + } } 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 7ead977b08..df805f5d33 100644 --- a/apps/desktop/src/renderer/locales/conversation-copy.ts +++ b/apps/desktop/src/renderer/locales/conversation-copy.ts @@ -1163,6 +1163,242 @@ const COPY = { }, turnError: { streamTruncated: 'The response stream ended before completion.', requestRejected: 'The model service rejected the request. Check the model and request configuration.', retryExhausted: 'The automatic retry limit was reached.', retryDeclined: { side_effects: 'Tool activity already occurred in this attempt. Automatic retry was declined to avoid repeating operations. Check the tool results first.', observable_output: 'This attempt already produced output, so it was not retried automatically. Check the retained content first.', policy: 'This attempt was not retried under the current retry policy.', budget: 'The execution budget was exhausted, so this attempt was not retried automatically.' }, unknown: 'Something went wrong; the cause is unknown.', contextOverflow: 'Context exceeded the model window. Reduce attachments or start a new task.', timeout: 'The model request timed out.', 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.', rateLimit: 'Requests were rate-limited.', network: 'The network connection failed. Check the network.', provider: 'The model service returned an error.', 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.' } }, }, + ko: { + actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The task action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh tasks', refreshSessionsFailedFallback: 'The task list could not be refreshed. Try again later.', conversationErrorTitle: 'Task error', conversationErrorFallback: 'The task run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: 'Branch created', branchCreatedDescription: (name) => `New task: ${name}`, revisionStartedTitle: 'Edit draft ready', revisionStartedDescription: 'The original task is kept; sending creates a new version', revisionReadyTitle: 'Ready to edit and resend', revisionReadyDescription: 'Rewound to before that message; edit and send when ready', revisionUnavailableTitle: 'This message cannot be edited yet', revisionAttachmentsUnsupported: 'Edit & resend does not yet support historical attachments. Copy the text into a new message instead.', revisionTransformedTextUnsupported: 'Edit & resend does not yet support messages sent with an explicit skill. Copy the text and select the skill again instead.', revisionDraftAttachmentConflict: 'The composer already has pending attachments. Send or remove them before editing a sent message.', revisionCommandUnsupported: 'You cannot run /compact, /side, or orchestration commands while editing a sent message. Cancel the edit first.', revisionAlreadyActive: 'Another message is already being edited. Send or cancel that edit first.', revisionCancelLabel: 'Cancel', revisionBannerTitle: 'Editing sent message', revisionBannerDetail: '· New version on send', revisionUnchanged: 'Nothing changed. Use Regenerate if you only want a new answer.', operationFailedTitle: 'Action failed', operationFailedFallback: 'The task action failed. Try again later.', attachmentFailedTitle: 'Failed to add attachment', imageAttachmentNotDirectTitle: 'Image added as an attachment', imageAttachmentNotDirectDescription: 'The current model does not receive images directly. The image has been provided as an attachment.', tryAgain: 'Try again later.', modelReboundTitle: 'Switched to an available model', modelReboundDescription: (modelId) => `The previous connection is unavailable${modelId ? ` · ${modelId}` : ''}`, messageReadFailedTitle: 'Failed to load task', partialHistoryTitle: 'Viewing earlier messages', returnLatest: 'Return to latest', scrollMainToBottom: 'Scroll main conversation to bottom' }, + attachments: { tooMany: 'You can attach at most 8 files', tooLarge: 'Attachments must be 50 MB or smaller', duplicate: 'This attachment was already added.' }, + model: { + fakeBackendLabel: 'Local simulation', + setupTitle: 'Configure a real model', + connectionMissingTitle: 'Connection deleted', + configurationFallback: 'This model connection cannot send right now. Check it in Settings · Models and try again.', + configurationReason: { + missing_default_connection: 'Set a default model in Settings · Models before sending.', + connection_missing: 'The model connection used by this task was deleted. Select or create one in Settings · Models.', + connection_disabled: 'The current model connection is disabled. Enable it or choose another default in Settings · Models.', + missing_api_key: 'The current model connection has no usable credentials. Add an API key or sign in again under Settings · Models.', + missing_model: 'The current connection has no usable model. Select a default model in Settings · Models.', + empty_model_list: 'The current connection has no enabled models. Add or enable one in Settings · Models.', + model_not_enabled: 'The model selected for this task is disabled. Choose an enabled model in Settings · Models.', + model_not_chat_capable: 'The model selected for this task cannot chat. Choose a chat-capable model in Settings · Models.', + fake_backend: 'This task used the retired local simulation. Add a real model in Settings · Models, then start a new task.', + provider_retired: 'The sign-in this task\u2019s connection uses was removed from Maka, so it cannot send. Switch to another connection in Settings · Models, then start a new task.', + }, + }, + footer: { labels: { regenerate: 'Regenerate', branch: 'Branch', copy: 'Copy', info: 'Details' }, pending: 'Working…', regenerateRunning: 'Wait for the current response to finish before regenerating', regenerateAgain: 'A regenerated response already exists; click again to create another parallel response', regenerate: 'Generate another response to this turn', branchRunning: 'Wait for the current response to finish before branching', branchAborted: 'Branch from the context before the interruption', branch: 'Branch a new task from this response', copy: 'Copy response to clipboard', copyEmpty: 'This response has no content to copy' }, + lineage: { regeneratedFrom: 'Regenerated from previous response', regeneratedFromTooltip: 'This is a parallel regenerated response; click to view the retained previous response', regeneratedTo: 'Regenerated → New response', regeneratedToTooltip: 'Jump to the regenerated response' }, + workbar: { + ariaLabel: 'Task workbar', + sectionsAriaLabel: 'Task workbar tabs', + review: 'Changes', + terminal: 'Terminal', + terminalNumbered: (index) => `Terminal ${index}`, + tasks: 'To-do', + todoLoadFailed: 'Failed to load the to-do list. Try again.', + workBoard: 'Work board', + browser: 'Browser', + files: 'Generated files', + inspector: 'Trace', + sideChat: 'Side chat', + sideChatNumbered: (index) => `Side chat ${index}`, + openTab: 'Open workbar tab', + openTools: 'Open tools', + closeTab: (label) => `Close ${label}`, + tabMenu: (label) => `${label} tab menu`, + moveLeft: 'Move left', + moveRight: 'Move right', + moveToRight: 'Move to right panel', + moveToBottom: 'Move to bottom panel', + pinTab: 'Pin tab', + pinTabHint: 'Preview tab. Double-click or interact with its content to pin it', + close: 'Close', + closeOthers: 'Close other tabs', + closeToRight: 'Close tabs to the right', + launcher: { + review: 'View changes in the current Git workspace', + terminal: 'Inspect terminal runs and live output for this task', + tasks: "View and maintain this task's to-do ledger", + workBoard: 'Capture and manage deferred work', + browser: 'Open the embedded browser and keep the current page', + files: 'Browse files generated by this task', + inspector: 'Inspect model calls, tools, and timing', + sideChat: 'Ask and explore read-only without interrupting the main task', + }, + }, + workBoardPanel: { + inbox: 'Inbox', + project: 'Current project', + noProject: 'No project selected', + createPlaceholder: 'Capture something for later…', + create: 'Add', + empty: 'No deferred work', + loading: 'Loading work board…', + loadMore: 'Load more', + retry: 'Retry', + loadFailed: 'Failed to load work board', + actionFailed: 'Action failed', + complete: 'Complete', + reopen: 'Reopen', + rename: 'Rename', + renameSave: 'Save', + moveToInbox: 'Move to Inbox', + moveToProject: 'Move to project', + archive: 'Archive', + unarchive: 'Restore', + delete: 'Delete', + archived: 'Archived', + }, + reviewPanel: { + ariaLabel: 'Git changes', + empty: 'No changes in the current Git workspace', + emptyHelp: 'Committed, staged, and modified files appear here.', + notGitRepository: 'This task directory is not a Git repository', + workspaceUnavailable: 'This task directory is unavailable', + unbornRepository: 'This Git repository has no commit to compare yet', + gitFailed: 'Could not read Git workspace changes', + invalidBaseBranch: 'The selected comparison branch is unavailable', + truncated: 'Too many changes; showing the first files only', + showMore: (remaining) => + `Show ${Math.min(20, remaining)} more file${Math.min(20, remaining) === 1 ? '' : 's'}`, + hiddenLines: (count) => + `${count} more line${count === 1 ? '' : 's'} not shown`, + changedFiles: (count) => `${count} changed file${count === 1 ? '' : 's'}`, + addedLines: (count) => `${count} line${count === 1 ? '' : 's'} added`, + deletedLines: (count) => `${count} line${count === 1 ? '' : 's'} deleted`, + added: (count) => `${count} added`, + deleted: (count) => `${count} deleted`, + loadFailed: 'Could not read Git changes', + retry: 'Retry', + }, + terminalPanel: { + ariaLabel: 'Task terminal', + empty: 'No terminal runs in this task yet', + emptyHelp: "This task's terminal appears here once it starts.", + loadFailed: 'Could not read terminal runs', + retry: 'Retry', + refresh: 'Refresh terminal', + readOnly: 'Shows terminal runs started by the agent or you in this task', + runCount: (count) => `${count} terminal run${count === 1 ? '' : 's'}`, + newTerminal: 'New terminal', + commandPlaceholder: 'Enter a command and press Enter', + commandLabel: 'Terminal command', + runCommand: 'Run command', + stopTerminal: 'Stop current terminal', + startFailed: 'Could not start terminal', + writeFailed: 'Could not send terminal input', + stopFailed: 'Could not stop terminal', + }, + inspector: { + ariaLabel: 'Task trace', + copyPricingKey: 'Copy pricing key', + pricingKeyCopied: 'Pricing key copied', + unpricedPricingKey: 'Unpriced pricing key', + copyFailed: 'Copy failed', + copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', + loadFailed: 'Could not read the trace', + retry: 'Retry', + empty: 'Nothing to trace in this task yet', + emptyHelp: 'No activity recorded for this task yet.', + costUnavailable: 'cost unknown', + costEstimateHelp: 'Estimated from recorded usage and pricing; missing or unpriced calls may be excluded.', + loadEarlier: 'Load earlier records', + hideEarlier: 'Hide all earlier records', + loadingEarlier: 'Loading…', + loadingTrace: 'Loading timeline…', + loadingSummary: 'Estimating full-session usage…', + summaryUnavailable: 'Full-session usage is temporarily unavailable.', + totals: { + cost: 'Estimated cost', + }, + coveragePartial: (parts) => + `Some calls could not be shown completely, so the numbers below only undercount${enDetail(parts)}`, + coverageAbsent: (parts) => `This backend does not record per-call detail${enDetail(parts)}`, + unreadable: (count) => `${count} record${count === 1 ? '' : 's'} could not be read`, + oversizedRuns: (count) => + `${count} run record${count === 1 ? '' : 's'} too large to show online`, + turnsMissing: (count) => `${count} turn${count === 1 ? '' : 's'} with no call record`, + turnsShort: (count) => + `${count} turn${count === 1 ? '' : 's'} with an incomplete call record`, + stepKind: { permission: 'Permission', compaction: 'Context compaction', error: 'Error' }, + callKind: (kind) => EN_CALL_KIND[kind as keyof CallKindCopy] ?? kind, + permissionDecision: (decision) => EN_PERMISSION_DECISION[decision] ?? decision, + recoveredAs: (disposition) => `recovered as ${disposition}`, + retries: (count) => `${count} retr${count === 1 ? 'y' : 'ies'}`, + turnFailure: (code) => EN_TURN_FAILURE[code] ?? 'Turn failed', + turnLabel: (startedAt) => `Turn · ${startedAt}`, + overview: { + context: 'Context window', + segment: { + cacheRead: 'Cache hit', + fresh: 'Cache miss', + used: 'Used', + free: 'Remaining', + }, + cacheHit: 'Cache hit rate', + timelineTab: 'Timeline', + composition: { + title: 'Estimated composition', + basis: 'Estimated from request bytes, not provider-reported tokens', + part: { + system_instructions: 'System instructions', + tool_definitions: 'Tool definitions', + messages: 'Messages', + other: 'Other options', + }, + tools: 'By tool', + remainingTools: (count) => `${count} more tool${count === 1 ? '' : 's'}`, + unlabelled: 'Unnamed tools', + unrecorded: 'This call left no composition on record', + }, + }, + }, + quoteCompanion: { + defaultName: 'Side chat', + namePrefix: 'Side: ', + preparing: 'Preparing side chat…', + permissionStreaming: 'Permissions cannot change while the side chat is running', + scrollToBottom: 'Scroll side conversation to bottom', + closeConfirmation: { + title: (count) => count > 1 ? `Close ${count} side chats?` : 'Close side chat?', + description: (count) => + count > 1 + ? `These ${count} temporary side chats will be permanently deleted and cannot be recovered.` + : 'This temporary side chat will be permanently deleted and cannot be recovered.', + dontAskAgain: 'Don’t ask again', + cancel: 'Cancel', + confirm: 'Close side chat', + }, + errors: { + forkSetupFailed: 'Could not open the side chat. Please try again.', + forkSourceBusy: + 'The main conversation or a linked task is still running. Try again when it finishes.', + forkUnsupported: 'This conversation context cannot be opened as a side chat yet.', + sendRejected: 'The companion could not start. Please try again.', + sendFailed: 'The companion request failed. Please try again.', + settlementFailed: 'The run ended, but its messages could not be loaded. Retry or reopen the side chat.', + respondFailed: 'The response failed. Please try again.', + }, + }, + health: { + blocked: { + fake_backend: { label: 'Stale task · Configure a real model', tooltip: () => 'This task used the retired local simulation. Add and enable a real model in Settings · Models before sending.' }, + provider_retired: { label: 'Sign-in retired', tooltip: (name) => `The sign-in that connection "${name}" uses was removed from Maka, so sending fails. Switch to another connection in Settings · Models.` }, + missing_default_connection: { label: 'No model configured', tooltip: () => 'This task has no available model connection. Add and enable one in Settings · Models.' }, + legacy_connection_identity: { label: 'Choose a model connection', tooltip: () => 'This task comes from an older version. Choose the connection and model to use.', actionLabel: 'Choose connection and model', settingsTooltip: () => 'No connections are currently available. Add or enable one in Settings · Models first.' }, + connection_missing: { label: 'Original connection deleted', tooltip: () => 'Choose a new connection and model to continue.', actionLabel: 'Choose connection and model', settingsTooltip: () => 'No connections are currently available. Add or enable one in Settings · Models first.' }, + connection_identity_mismatch: { label: 'Connection identity mismatch', tooltip: () => 'Choose the connection and model to use again.', actionLabel: 'Choose connection and model', settingsTooltip: () => 'No connections are currently available. Add or enable one in Settings · Models first.' }, + connection_disabled: { label: 'Connection disabled', tooltip: (name) => `Connection "${name}" is disabled. Enable it or choose another connection in Settings · Models.` }, + missing_api_key: { label: 'Connection credentials missing', tooltip: (name) => `Connection "${name}" has no API key or completed sign-in. Add credentials in Settings · Models.` }, + missing_model: { label: 'No model selected', tooltip: (name) => `Connection "${name}" has no default model. Select one in Settings · Models.` }, + empty_model_list: { label: 'No models enabled', tooltip: (name) => `Connection "${name}" has no enabled models. Add one in Settings · Models.` }, + model_not_enabled: { label: 'Task model disabled', tooltip: (name, model) => `Model "${model}" is not enabled for connection "${name}". Choose another model in Settings · Models.` }, + model_not_chat_capable: { label: 'Task model cannot chat', tooltip: (_name, model) => `Model "${model}" cannot be used for chat. Choose a chat-capable model in Settings · Models.` }, + }, + connectionChoicesLoading: { tooltip: 'The connection list has not loaded yet.', actionLabel: 'Reload connections' }, + reauth: { label: 'Last connection test failed authentication', tooltip: 'The latest test returned 401 / 403. Sending is not blocked, but sign in again under Settings · Models if it fails.' }, + testError: { label: 'Last connection test failed', tooltip: 'The latest test failed because of a network, timeout, or 5xx error. Sending is not blocked; check Base URL or proxy settings if it persists.' }, + }, + 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.', contextBudgetExhausted: 'The context limit was reached and this task cannot continue. Switch models or start a new task.', malformedSummary: 'Context compaction could not produce a valid summary. Check the model context-window setting, switch models, 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.' } }, + } } 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 cbbb03db71..8e20dd53bd 100644 --- a/apps/desktop/src/renderer/locales/external-session-import-copy.ts +++ b/apps/desktop/src/renderer/locales/external-session-import-copy.ts @@ -273,6 +273,61 @@ 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: 'Source', + sourceNames: { codex: 'Codex', 'claude-code': 'Claude Code' }, + includeArchived: 'Include archived conversations', + searchLabel: 'Search', + searchHelp: 'Matches the conversation title and the project path. Empty shows everything.', + searchPlaceholder: 'Part of a title or path', + searchEmpty: (term) => `No conversation has "${term}" in its title or path.`, + loading: 'Reading external conversations…', + listAria: 'Conversations available to import', + emptyTitle: 'No conversations to import', + emptyDescription: 'No matching root conversations were found in this source.', + unavailableTitle: 'No supported Agent detected', + unavailableDescription: + 'Once Codex has been used on this machine, its conversations appear here. Maka only reads those files and never modifies them.', + loadFailedTitle: 'Could not read external conversations', + loadFailedFallback: 'The external session directory is temporarily unavailable. Try again.', + retry: 'Retry', + archived: 'Archived', + loadMore: 'Load more', + loadingMore: 'Loading…', + duplicateNote: 'Importing the same conversation again creates an independent task.', + importedCount: (count) => (count === 1 ? 'Imported once' : `Imported ${count} times`), + openLatestImportedTask: 'Open latest imported task', + openLatestImportedTaskFor: (name) => `Open the latest task imported from ${name}`, + import: 'Import', + importAgain: 'Import again', + importTask: (name) => `Import ${name}`, + importTaskAgain: (name) => `Import ${name} again`, + importing: 'Importing…', + importingTask: (name) => `Importing ${name}`, + importInProgressTitle: 'Import in progress', + importInProgressDescription: (name) => + `Importing “${name}”. Maka opens the task as soon as it lands.`, + importFailedTitle: 'Import failed', + importFailedFallback: 'This conversation could not be converted or saved. Check the source and try again.', + importRecoveredTitle: 'Import confirmed', + importRecoveredDescription: (name) => + `The imported task is available now for “${name}”.`, + importNotRecordedTitle: 'No new task found', + importNotRecordedDescription: 'No new task was recorded, so it is safe to retry.', + importOutcomeUnknownTitle: 'Check the import result', + selectAllAriaLabel: 'Select all or none', + selectedCount: (selected, listed) => `${selected} / ${listed} selected`, + selectRowAriaLabel: (name) => `Select ${name}`, + importSelected: 'Import selected', + batchProgress: (done, total) => `Importing ${done} / ${total}`, + batchDoneTitle: (imported) => `Imported ${imported} conversations`, + batchDuplicated: (count) => + `${count} of them had been imported before and now exist twice.`, + batchFailed: (count) => `${count} more could not be imported.`, + batchNothingImported: 'No conversation was imported.', + 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.`, + } } 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 8eb2ad8aaf..5c7f5849e0 100644 --- a/apps/desktop/src/renderer/locales/mcp-copy.ts +++ b/apps/desktop/src/renderer/locales/mcp-copy.ts @@ -248,6 +248,65 @@ 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: 'Failed to load MCP', install: (name) => `Failed to install ${name}`, cancelInstall: (name) => `Failed to cancel installation of ${name}`, save: 'Failed to save MCP', + import: 'Failed to import MCP', update: 'Failed to update MCP', test: 'MCP test failed', remove: 'Failed to delete MCP', unavailableStatus: 'The server did not return an available status.', + mapLine: (line) => `Line ${line} must use KEY=value`, importJson: 'MCP configuration must be valid JSON', importObject: 'MCP JSON must be an object', + importVersion: (version) => `Unsupported MCP config version ${version}; versions 1, 2, and 3 are supported`, importServersObject: 'mcpServers must be an object', + importProtocolVersion: 'Remote protocol preferences require version 2 or 3; stdio protocol preferences require version 3', + }, + toast: { + templateInstalled: (name) => `${name} template installed`, templateInstalledDetail: 'Finish configuring credentials under Installed before enabling the connection.', + installed: (name) => `${name} installed`, installedDetail: 'Discovered tools take effect from the next agent turn.', installCancelled: (name) => `Cancelled installation of ${name}`, + saved: 'MCP saved', savedDetail: 'New tools take effect from the next agent turn.', imported: 'MCP imported', importedDetail: (count) => `Imported ${count} ${count === 1 ? 'server' : 'servers'}.`, + connectionOk: 'MCP connection healthy', toolLatency: (count, latencyMs) => `${count} ${count === 1 ? 'tool' : 'tools'} · ${latencyMs} ms`, + connectionFailed: 'MCP connection failed', removed: 'MCP deleted', + }, + remove: { title: (id) => `Delete MCP “${id}”?`, description: 'Its tools will be removed from the next agent turn, and the configuration cannot be restored automatically.', confirm: 'Delete', cancel: 'Cancel' }, + page: { + actionsAria: 'MCP actions', refreshing: 'Refreshing…', refresh: 'Refresh', add: 'Add MCP', + metaInstalled: (count) => `${count} installed`, metaErrors: (count) => `${count} ${count === 1 ? 'connection error' : 'connection errors'}`, + searchMatches: (count) => `${count} ${count === 1 ? 'match' : 'matches'}`, + workspaceAria: 'MCP marketplace and installed servers', toolbarAria: 'MCP browser controls', setupTitle: 'Connect Maka to your work environment', setupDescription: 'Start with a curated template, or add any stdio, Streamable HTTP, or SSE server.', + localStdio: 'Local stdio', categoriesAria: 'MCP categories', market: 'Marketplace', installed: 'Installed', + searchPlaceholder: 'Search MCP…', searchAria: 'Search MCP', noMarket: 'No matching MCP servers', noMarketDetail: (query) => `Try another keyword, or clear “${query}” to view every template.`, + clearSearch: 'Clear search', loading: 'Reading MCP configuration…', noInstalled: 'No MCP servers installed', noInstalledDetail: 'Choose a template from the marketplace, or add your own server manually.', + browseMarket: 'Browse marketplace', noInstalledMatch: 'No matching installed MCP servers', noInstalledMatchDetail: (query) => `Try another keyword, or clear “${query}” to view every installed server.`, + }, + detail: { + label: 'Server details', enabled: 'Enabled', transport: 'Transport', endpoint: 'Endpoint', + toolsLabel: 'Tools', statusLabel: 'Status', protocolLabel: 'MCP protocol', + negotiatedProtocol: (era, revision) => `${era === 'modern' ? 'Modern' : 'Legacy'} · ${revision}`, + inspectorOpened: (id) => `${id} details opened`, + }, + card: { + macOnly: 'macOS only', manage: 'Manage', cancellingAria: (name) => `Cancelling installation of ${name}`, cancelAria: (name) => `Cancel installation of ${name}`, installAria: (name) => `Install ${name}`, + cancelling: 'Cancelling…', cancel: 'Cancel installation', install: 'Install', + }, + row: { + testing: 'Testing…', test: 'Test', edit: 'Edit', + delete: 'Delete', tools: (count) => `${count} ${count === 1 ? 'tool' : 'tools'}`, + disabled: 'Disabled', disconnected: 'Disconnected', connecting: 'Connecting', connected: (count) => `${count} ${count === 1 ? 'tool' : 'tools'}`, failed: 'Connection failed', + }, + editor: { + importTitle: 'Import from JSON', editTitle: (id) => `Edit ${id}`, addTitle: 'Add MCP', importSubtitle: 'Paste an mcpServers configuration; servers with matching names will be updated.', + manualSubtitle: 'Configuration is saved in mcp.json for the current workspace.', modeAria: 'MCP add method', manual: 'Manual configuration', pasteJson: 'Paste JSON', jsonConfig: 'JSON configuration', + jsonHelp: 'Supports a complete mcpServers configuration or a server map. Existing MCP servers omitted from this import are preserved.', cancel: 'Cancel', importConnect: 'Import and connect', + transportAria: 'Connection method', localStdio: 'Local stdio', remoteUrl: 'Remote URL', + serverId: 'Server ID', command: 'Command', + commandPlaceholder: 'npx -y @modelcontextprotocol/server-filesystem /path/to/folder', + commandHelp: 'Full command line; quote arguments containing spaces. Not interpreted by a shell.', + workingDirectory: 'Working directory', workingDirectoryPlaceholder: 'Optional, for example /path/to/project', + environment: 'Environment', environmentHelp: 'One KEY=value entry per line; complete the variables required by this MCP.', url: 'MCP URL', headers: 'HTTP headers', headersHelp: 'One Header=value entry per line.', + saveConnect: 'Save and connect', + required: 'This field is required.', invalidUrl: 'Enter a valid HTTP or HTTPS URL.', unbalancedQuote: 'Unclosed quote.', + transportLabel: 'Transport', transportAuto: 'Auto fallback', transportStreamableHttp: 'Streamable HTTP', transportLegacySse: 'Legacy SSE', + protocolLabel: 'Protocol preference', protocolLegacy: 'Legacy', protocolAuto: 'Auto-negotiate', protocolModern: '2026-07-28 only', + protocolHelp: 'Existing configurations default to legacy; auto-negotiation selects an era from the server response.', sseProtocolHelp: 'Legacy SSE supports only the legacy protocol era.', expandAdvanced: 'Show advanced settings', collapseAdvanced: 'Hide advanced settings', + 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.', + }, + } } 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 f408b0ab3b..d5c1809acb 100644 --- a/apps/desktop/src/renderer/locales/onboarding-copy.ts +++ b/apps/desktop/src/renderer/locales/onboarding-copy.ts @@ -198,6 +198,9 @@ const ONBOARDING_COPY_BY_LOCALE: UiCatalog = { skip: 'Skip onboarding', snapshotErrorFallback: 'First-run status is temporarily unavailable. Try again later.', }, + get ko() { + return this.en; + }, }; 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 e2398a71eb..6b35b511e7 100644 --- a/apps/desktop/src/renderer/locales/permission-center-copy.ts +++ b/apps/desktop/src/renderer/locales/permission-center-copy.ts @@ -272,6 +272,53 @@ const PERMISSION_CENTER_COPY = { } satisfies Record)[health], reasonFallback: 'See the runtime logs for details.', }, + 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 29f000c729..d1fcb7bc6f 100644 --- a/apps/desktop/src/renderer/locales/plan-mode-copy.ts +++ b/apps/desktop/src/renderer/locales/plan-mode-copy.ts @@ -108,6 +108,26 @@ const COPY = { stepStatuses: { pending: 'Not started', in_progress: 'In progress', completed: 'Completed', skipped: 'Skipped' }, }, }, + ko: { + abandonConfirmation: { + title: 'Abandon this plan?', + description: (title) => `The execution record for “${title}” will remain, but it cannot be resumed.`, + confirm: 'Abandon plan', + cancel: 'Cancel', + }, + proposal: { + aria: 'Plan proposal', kicker: 'Plan proposal', revision: 'Revision', steps: 'Steps', + risks: 'Risks', revise: 'Request changes', execute: 'Execute plan', + statuses: { pending_approval: 'Waiting for approval', approved: 'Approved', stale: 'Outdated' }, + }, + execution: { + aria: 'Plan execution status', interrupted: 'Plan interrupted', running: 'Executing plan', + approvedPlan: 'Approved plan', + stepCount: (completed, total) => `${completed}/${total} ${total === 1 ? 'step' : 'steps'}`, + resume: 'Resume', abandon: 'Abandon plan', + stepStatuses: { pending: 'Not started', in_progress: 'In progress', completed: 'Completed', skipped: 'Skipped' }, + }, + } } satisfies UiCatalog; export function getPlanModeCopy(locale: UiLocale): PlanModeCopy { diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts index 22052bb196..b096ebfa15 100644 --- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts +++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts @@ -346,7 +346,7 @@ const EN = { pendingTurnRequestCount: (count: number) => `${count} pending Turn ${count === 1 ? 'request' : 'requests'}`, } satisfies SessionCollaborationCopy; -const COPY = { 'zh-CN': ZH_CN, 'zh-TW': ZH_TW, en: EN } satisfies UiCatalog; +const COPY = { 'zh-CN': ZH_CN, 'zh-TW': ZH_TW, 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 a94f960ba7..a9d245382d 100644 --- a/apps/desktop/src/renderer/locales/settings-bot-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-bot-copy.ts @@ -423,6 +423,7 @@ const BOT_SETTINGS_COPY = { 'zh-CN': zhCopy, 'zh-TW': zhTwCopy, en: enCopy, + ko: enCopy, } satisfies UiCatalog; export function getBotSettingsCopy(locale: UiLocale): BotSettingsCopy { 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 489ba80480..0b6521cfef 100644 --- a/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-daily-review-copy.ts @@ -97,6 +97,25 @@ const SETTINGS_DAILY_REVIEW_COPY = { model: 'Analysis model', modelHelp: 'Follows the current task default when unspecified.', }, + ko: { + 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}`, + scheduleTitle: 'Schedule', + scheduleDescription: 'Analyze the previous complete local day automatically.', + enabled: 'Enable scheduled analysis', + enabledHelp: 'Generate an analysis of yesterday’s activity each day.', + executeTime: 'Run time', + executeTimeHelp: 'Uses your local time in 24-hour format.', + executeTimePlaceholder: 'HH:mm', + executeTimeInvalid: 'Enter a 24-hour time, for example 08:00.', + analysisTitle: 'Analysis', + analysisDescription: 'Choose the model used to generate the fixed report structure.', + model: 'Analysis model', + modelHelp: 'Follows the current task default when unspecified.', + } } satisfies UiCatalog; export function getDailyReviewSettingsCopy(locale: UiLocale): DailyReviewSettingsCopy { diff --git a/apps/desktop/src/renderer/locales/settings-data-copy.ts b/apps/desktop/src/renderer/locales/settings-data-copy.ts index b8bce4bc15..1d2948bd3b 100644 --- a/apps/desktop/src/renderer/locales/settings-data-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-data-copy.ts @@ -125,6 +125,33 @@ const SETTINGS_DATA_COPY = { 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…', }, + ko: { + 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 }, + }, + 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', + }, + 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.', + }, + 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…', + } } satisfies UiCatalog; export function getDataSettingsCopy(locale: UiLocale): DataSettingsCopy { diff --git a/apps/desktop/src/renderer/locales/settings-health-copy.ts b/apps/desktop/src/renderer/locales/settings-health-copy.ts index b4f9229b28..1ae614eca8 100644 --- a/apps/desktop/src/renderer/locales/settings-health-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-health-copy.ts @@ -163,6 +163,25 @@ const SETTINGS_HEALTH_COPY = { signalMessage: (signal) => signalMessagesEn[signal.message], signalDetail: (signal) => signalDetailEn(signal.detail), }, + ko: { + 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`, + }, + 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', + signalLabel: englishSignalLabel, + signalMessage: englishSignalMessage, + signalDetail: englishSignalDetail, + } } satisfies UiCatalog; export function getHealthCenterCopy(locale: UiLocale): HealthCenterCopy { diff --git a/apps/desktop/src/renderer/locales/settings-memory-copy.ts b/apps/desktop/src/renderer/locales/settings-memory-copy.ts index aeaa911bb6..63c0500b0c 100644 --- a/apps/desktop/src/renderer/locales/settings-memory-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-memory-copy.ts @@ -228,6 +228,53 @@ const SETTINGS_MEMORY_COPY = { }, 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]', }, + ko: { + intlLocale: 'ko-KR', + text: enText, + countActive: (count, draft) => draft + ? `Draft · ${count} active ${count === 1 ? 'entry' : 'entries'}` + : `${count} active ${count === 1 ? 'entry' : 'entries'}`, + countArchived: (count, draft) => draft + ? `Draft · ${count} archived ${count === 1 ? 'entry' : 'entries'}` + : `${count} archived ${count === 1 ? 'entry' : 'entries'}`, + saveSummary: (active, archived) => archived > 0 + ? `${active} active ${active === 1 ? 'entry' : 'entries'} / ${archived} archived ${archived === 1 ? 'entry' : 'entries'}; the previous version was backed up.` + : `${active} active ${active === 1 ? 'entry' : 'entries'}; the previous version was backed up.`, + backupSummary: (active, archived) => archived > 0 + ? `${active} active ${active === 1 ? 'entry' : 'entries'} / ${archived} archived ${archived === 1 ? 'entry' : 'entries'}` + : `${active} active ${active === 1 ? 'entry' : 'entries'}`, + countEntries: (count) => count === 1 ? `${count} memory` : `${count} memories`, + countMatches: (filtered, total) => `${filtered} / ${total} matching`, + listAria: (title) => `${title} list`, + entryActionsAria: (title) => `${title} memory actions`, + entryActionAria: (action, identity) => `${action}: ${identity}`, + openBackupAria: (label) => `Open backup candidate ${label}`, + restoreBackupAria: (label) => `Restore backup candidate ${label}`, + copyBackupAria: (label) => `Copy backup candidate reference ${label}`, + draftStatusAria: (action) => `${action}; MEMORY.md is not written until you save`, + restoreLatestDescription: (label) => `The current MEMORY.md will be backed up before the latest backup replaces it. Restore: ${label}`, + restoreCandidateDescription: (label) => `The current MEMORY.md will be backed up before the selected backup replaces it. Restore: ${label}`, + redactedDetail: (summary) => `Suspected tokens, API keys, or passwords were redacted before writing; ${summary}`, + openBackupFailed: (kind) => `Failed to open ${kind}`, + previewTruncated: (limit) => `Preview truncated at the ${limit}-character limit`, + previewUsage: (length, limit) => `Preview ${length} / ${limit} characters`, + previewLimit: (limit) => `Prompt limit: ${limit} characters`, + results: { + no_backup: 'No MEMORY.md backup is available.', invalid_backup_kind: 'Unrecognized backup kind.', + memory_unavailable: 'Local memory is currently unavailable.', backup_not_found: 'The backup file was not found.', + remote_host_owned: 'Memory files are owned by the remote Runtime Host and cannot be opened locally.', not_regular_file: 'The memory path is not an allowed regular file.', + open_failed: 'The system could not open the memory file.', file_not_found: 'The memory file was not found.', + disabled: 'Local memory is disabled.', incognito_active: 'Unavailable in incognito mode.', + safe_mode: 'MEMORY.md is too large and entered safe mode.', oversize: 'MEMORY.md exceeds the safety limit. Remove older content first.', + revision_conflict: 'Memory was just changed by another operation. Try again.', backup_revision_conflict: 'The backup was just changed by another operation. Try again.', + invalid_state: 'The Runtime Host returned an invalid memory state.', + invalid_content: 'MEMORY.md content is invalid. Check its format and try again.', invalid_scope: 'The memory operation has an invalid scope.', + not_found: 'The memory entry was not found.', not_pending: 'The memory entry is not pending review.', + upload_not_found: 'The memory upload session does not exist or has expired.', upload_incomplete: 'The memory content has not finished uploading.', + upload_conflict: 'Another memory upload is in progress. Try again.', + }, + 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]', + }, } satisfies UiCatalog; export function getMemorySettingsCopy(locale: UiLocale): MemorySettingsCopy { return SETTINGS_MEMORY_COPY[locale]; } diff --git a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts index 6d83b15c5b..5fdaafffbb 100644 --- a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts @@ -105,6 +105,32 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { about: { label: 'About', description: 'Version, updates, and support.' }, }, }, + ko: { + groups: { + preferences: 'Preferences', + capabilities: 'Capabilities', + activity: 'Activity', + 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, runtime environment, and privacy commitments.' }, + }, + } } satisfies UiCatalog; export function getSettingsNavigationCopy(locale: UiLocale): SettingsNavigationCopy { diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts index a065bc83d3..f9896ebb69 100644 --- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts @@ -519,6 +519,59 @@ 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: '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-CN', 'Simplified Chinese'], ['zh-TW', 'Traditional Chinese'], ['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.', + }, + 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.', + }, + 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', packagedBuild: 'Release build', subtitle: 'A local-first AI assistant · Desktop runtime', privacyLabel: 'Privacy and security', privacyTitle: 'Local first · Private by default', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay in the local workspace.', 'Model keys stay in a local credential file; 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.'], copying: 'Copying…', copyDiagnostics: 'Copy diagnostics', copyHelp: 'Copy version, platform, a home-redacted workspace path, and recent redacted Desktop and Runtime Host logs. The report is written only to the clipboard and is never uploaded automatically.', keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', reportIssueLabel: 'Report an issue', + updatesTitle: 'Software updates', + checkForUpdates: 'Check for updates', + checkingForUpdates: 'Checking…', + updateHelp: 'Maka also checks in the background. When a restart is required, the sidebar will prompt you.', + updateDevBuildHelp: 'Development builds do not check GitHub releases. Use a packaged install.', + 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 78ee694394..a226b3ed21 100644 --- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts @@ -1363,6 +1363,317 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { '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}`, }, + ko: { + runtimeHost: { + title: 'Runtime Host', + description: 'Local and other enabled Hosts stay connected together. Each task remains owned by its Host.', + selected: 'Default Host', + selectedHelp: 'New tasks and unscoped settings use the default Host', + remoteTitle: 'Other Hosts', + remoteDescription: + 'Set up a Runtime Host on an SSH computer or local WSL environment, or connect an existing Host manually.', + addComputer: 'Add computer', + useConnectionCode: 'Use connection code', + configureManually: 'Configure manually', + thisComputerRemoteAccess: 'Remote access', + thisComputerRemoteAccessHelp: 'Reach this Host through experimental end-to-end direct connections, with automatic public coordination discovery', + remoteAccessOn: 'On', + remoteAccessOff: 'Off', + enableRemoteAccess: 'Enable', + disableRemoteAccess: 'Turn off connectivity', + disableRemoteAccessConfirm: 'Turn off remote connectivity?', + disableRemoteAccessDescription: 'This only stops Direct peer connectivity. Granted shared access is retained.', + revokeSharedAccess: 'Revoke shared access', + revokeSharedAccessConfirm: 'Revoke shared access?', + revokeSharedAccessDescription: 'The connected Desktop will be disconnected, and unused connection codes will stop working.', + revokeSharedAccessDone: 'Shared access revoked', + createConnectionCode: 'New connection code', + connectionCodeTitle: 'Connect to this computer', + connectionCodeDescription: 'Expires in 15 minutes and can be used once. The other Desktop receives Owner access. Direct peer has no fallback.', + importConnectionCodeTitle: 'Use a connection code', + importConnectionCodeDescription: 'Connecting grants this Desktop Owner access to the other Host. Direct peer has no fallback.', + connectionCode: 'Connection code', + copyConnectionCode: 'Copy connection code', + connectionCodeCopied: 'Connection code copied', + connectionCodeInvalid: 'The connection code is invalid.', + connectionCodeUnavailable: 'The connection code expired or was already used. Create a new code on the other computer.', + connectionCodeHostUnreachable: 'A Direct peer connection could not be established. Check that both computers are online and UDP is allowed.', + connectionCodeHostMismatch: 'The code does not match the connected Host, or the Host version is incompatible.', + connectionCodeUnknownError: 'The connection outcome is unknown. Check the remote Host list before retrying.', + connectWithCode: 'Connect', + remoteAccessActiveTasks: 'This computer still has running tasks', + remoteAccessActiveTasksDescription: 'Enabling remote access hands the Local Host to a system service. Interrupt the current tasks and continue?', + uninstallActiveTasksDescription: 'Removing the background service stops the current tasks. Interrupt them and continue?', + interruptAndEnable: 'Interrupt and enable', + interruptAndUninstall: 'Interrupt and remove', + remoteAccessFailed: 'Remote access failed', + setupTitle: 'Add Runtime Host', + setupDescription: 'Install and connect Runtime Host on an SSH computer or WSL environment', + setupName: 'Display name (optional)', + setupTarget: 'Run on', + sshComputer: 'SSH computer', + wslEnvironment: 'WSL environment', + wslDistribution: 'WSL distribution', + setupSshPort: 'SSH port (optional)', + setupDirectoryRootsDescription: 'Leave empty to use the remote Home directory. When directories are added, only those locations can be browsed to add projects.', + setupConnect: 'Connect', + setupCancel: 'Cancel', + setupRetry: 'Retry', + setupDone: 'Done', + setupChooseProject: 'Choose project', + setupComplete: 'Runtime Host connected', + setupPhase: { + preparing_cli: 'Preparing the local CLI…', + connecting_ssh: 'Connecting over SSH…', + connecting_wsl: 'Connecting to the WSL environment…', + checking_environment: 'Checking the remote environment…', + installing_package: 'Installing Maka…', + installing_service: 'Starting Runtime Host…', + pairing_client: 'Pairing this device…', + verifying_connection: 'Verifying access…', + connecting_host: 'Establishing the secure connection…', + }, + add: 'Add remote Host', + cancel: 'Cancel', + name: 'Display name', + nameHelp: 'Used only to identify this Host on this device', + transport: 'Connection method', + transportHelp: 'Prefer TLS, or use an SSH tunnel to reach a loopback-only Host on a private machine', + tls: 'TLS', + ssh: 'SSH tunnel', + plaintext: 'Plain WebSocket', + url: 'WSS URL', + urlHelp: 'The wss:// address of the remote Runtime Host', + plaintextUrl: 'WS URL', + plaintextUrlHelp: 'The ws:// address of the remote Runtime Host', + sshDestination: 'SSH destination', + sshDestinationHelp: 'An OpenSSH user@host destination or SSH config alias', + sshPort: 'SSH port', + sshPortHelp: 'Optional; leave empty to use the OpenSSH default or SSH config', + remotePort: 'Remote Host port', + remotePortHelp: 'WebSocket port where Runtime Host listens on 127.0.0.1 remotely', + websocketPath: 'WebSocket path', + websocketPathHelp: 'Usually /runtime-host', + plaintextAcknowledgement: 'I understand the plaintext risk', + plaintextAcknowledgementHelp: 'Access credentials and data may be intercepted by others on the network', + plaintextWarning: 'Use only on a trusted, isolated network. Public connections should use TLS or an SSH tunnel.', + sshTerminalTitle: 'Connect to remote Runtime Host', + sshTerminalDescription: 'Follow the OpenSSH prompt to trust the Host or enter a password. Existing SSH keys normally need no input.', + sshTerminalClosed: 'The SSH connection ended', + sshTerminalClose: 'Close', + rootId: 'State Root ID', + rootIdHelp: 'Copied from the remote service ready output to verify the expected Host', + credential: 'Access credential', + credentialHelp: 'Issue it on the remote machine with the desktop-client preset', + saveAndEnable: 'Save and enable', + defaultBadge: 'Default', + experimentalBadge: 'Experimental', + defaultDisableHelp: 'Choose another default Host before disabling this Host', + unavailable: 'Unavailable', + manage: 'Manage', + managementTitle: (name: string) => `Manage ${name}`, + serviceStatus: 'Service status', + serviceState: { + not_installed: 'Not installed', + stopped: 'Stopped', + starting: 'Starting', + running: 'Running', + 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.', + directPeerState: { + unsupported: 'Update required', + not_configured: 'Not configured', + disabled: 'Disabled', + enabled: 'Enabled', + unavailable: 'Unavailable', + }, + directPeerUnavailable: 'Direct peer status is unavailable', + directPeerUpgradeRequired: 'Update the remote Runtime Host before managing Direct peer.', + directPeerClientUnavailable: 'This Desktop build does not include Direct peer support.', + directPeerDisableProfileFirst: 'Disable the Direct peer in the Runtime Host list first.', + directPeerId: 'Peer ID', + directPeerRoutes: 'Routes', + directPeerCoordinationRelays: 'Connection coordination peers (optional)', + directPeerCoordinationRelaysPlaceholder: 'Separate multiple addresses with commas', + directPeerAdvancedCoordination: 'Set coordination peers manually', + directPeerAdvancedNatTraversal: 'NAT traversal (advanced)', + directPeerStunPolicy: 'Public address discovery', + directPeerStunPolicyOptions: { + default: 'Public STUN (recommended)', + disabled: 'No public 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.', + 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.', + directPeerEnable: 'Enable and add', + directPeerDisable: 'Disable', + directPeerAddProfile: 'Add to Desktop', + directPeerActionFailed: 'Direct peer action failed', + peerMesh: 'Peer Mesh', + peerMeshHelp: 'Manage private Mesh memberships and invitations for this Desktop peer', + managePeerMesh: 'Manage Peer Mesh', + installedVersion: 'Version', + operatingSystem: 'System', + processId: 'Process ID', + lastExitCode: 'Last exit code', + stateRoot: 'State Root', + directoryRoots: 'Directories for adding projects', + directoryRootsDescription: 'Remote Clients can browse and add new projects only from these directories. Removing one does not delete projects already added.', + directoryRootsUnavailable: 'Update or repair this Host to manage these directories in Desktop.', + directoryRootsChanged: 'These directories changed elsewhere', + directoryRootsChangedDescription: 'Your draft is preserved. Load the current configuration before continuing.', + reloadDirectoryRoots: 'Load current configuration', + noDirectoryRoots: 'Directory browsing and project registration are disabled', + directoryRootLabel: 'Display name', + directoryRootPath: 'Absolute path on remote computer', + addDirectoryRoot: 'Add directory', + removeDirectoryRoot: 'Remove', + saveDirectoryRoots: 'Apply directories', + directoryRootsActiveTasks: 'This Host still has running tasks', + directoryRootsActiveTasksDescription: 'Applying these directories requires a safe remote service restart. Tasks are interrupted only after explicit confirmation.', + configureDirectoriesInterrupt: 'Interrupt tasks and apply', + refresh: 'Refresh', + startService: 'Start', + restartService: 'Restart', + restartActiveTasksDescription: 'Restarting stops the current tasks. Interrupt them and continue?', + restartInterrupt: 'Interrupt tasks and restart', + repairService: 'Repair', + updateService: 'Install matching version', + updatePolicy: 'Update policy', + updatePolicyDescription: 'Choose which Maka release this Host follows', + updatePolicyManual: 'Manual', + updatePolicyAutomatic: 'Automatic', + updatePolicyOptions: { + manual: 'Manual updates', + fixed: 'Fixed version', + latest: 'Latest stable 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', + updateSchedulerUnsupported: 'Unsupported', + updateSchedulerInactive: 'Inactive', + 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', + 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`, + updatePolicyManualAction: (version: string) => `Maka ${version} needs a manual update`, + 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', + }, + 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…', + }, + 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.', + updateInterrupt: 'Interrupt and update', + updateComplete: (from: string, to: string) => `Runtime Host was updated from ${from} to ${to}`, + updateRepaired: (version: string) => `Runtime Host ${version} is running again`, + updateAlreadyCurrent: (version: string) => `Runtime Host is already on ${version}`, + showLogs: 'View logs', + noLogs: 'No service logs were found', + uninstallService: 'Uninstall service', + uninstallConfirmTitle: 'Uninstall this Runtime Host?', + uninstallConfirmBody: 'This stops and removes the Maka-managed service and program, while preserving the State Root, projects, and task data. The Desktop profile is not removed.', + uninstallConfirm: 'Uninstall service', + uninstallRetained: (path: string) => `Service uninstalled. Data was retained at ${path}`, + managementActionFailed: 'Unable to manage the Runtime Host service', + managementReconnectFailed: 'Change applied, but Desktop could not reconnect', + manageAccess: 'Manage access', + accessTitle: 'Access', + noAccessCredentials: 'No active access credentials', + currentDesktop: 'This Desktop', + accessKind: { + owner: 'Client access', + capabilityProvider: 'Capability provider', + }, + accessPending: 'Pending confirmation', + accessCreated: (date: string) => `Created ${date}`, + rotateCredential: 'Rotate credential', + rotateCredentialConfirmTitle: 'Rotate this Desktop credential?', + rotateCredentialConfirmBody: 'Rotation reconnects this Runtime Host and may interrupt active work. Finish or pause active tasks before continuing.', + rotateCredentialConfirm: 'Continue rotation', + enableBeforeRotate: 'Enable this Runtime Host before rotating this Desktop credential.', + startBeforeChangingAccess: 'Start the Runtime Host service before changing access.', + revokeCredential: 'Revoke', + revokeCredentialConfirm: (name: string) => `Revoke access for ${name}?`, + revokeCredentialConfirmBody: 'Clients using this credential disconnect immediately, which may interrupt active work.', + accessActionFailed: 'Unable to manage access', + back: 'Back', + remove: 'Remove', + empty: 'No remote Hosts yet', + loadFailed: 'Could not load Runtime Host profiles', + selectFailed: 'Could not update the Runtime Host', + saveFailed: 'Could not save the Runtime Host profile', + removeFailed: 'Could not remove the Runtime Host profile', + pairingRecoveryTitle: 'Pairing is unfinished', + pairingRecoveryDescription: 'Retry from the affected Host menu, or discard the pairing to clean up the unfinished connection.', + resolvePairingRecovery: 'Retry pairing', + resolvePairingRecoveryFailed: 'Could not resolve pairing recovery', + pairingPendingBadge: 'Pairing unfinished', + discardPairing: 'Discard pairing', + 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}`, + }, + 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.', + addProject: 'Add project', + defaultBadge: 'Default', + setDefault: 'Set as default', + setDefaultTitle: 'Open new tasks in this project', + setDefaultDisabledTitle: 'The folder is unavailable, so this cannot be the default', + setDefaultFailed: 'Could not set the default project', + rename: 'Rename', + renameLabel: 'Project name', + renameFailed: 'Could not rename the project', + openFolder: 'Open project folder', + openFolderFailed: 'Could not open this folder — it may have been moved or deleted', + save: 'Save', + cancel: 'Cancel', + 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.', + 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.', + 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}`, + } } satisfies UiCatalog; export function getSettingsProjectsCopy(locale: UiLocale): SettingsProjectsCopy { diff --git a/apps/desktop/src/renderer/locales/settings-shared-copy.ts b/apps/desktop/src/renderer/locales/settings-shared-copy.ts index 0b087c10e9..854338f7e2 100644 --- a/apps/desktop/src/renderer/locales/settings-shared-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-shared-copy.ts @@ -188,6 +188,48 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { reviewScheduleHelp: 'When the daily review runs, and which model writes it.', }, }, + ko: { + modalLabel: 'Settings', + contentLabel: 'Settings content', + sidebarLabel: 'Settings sidebar', + navigationLabel: 'Settings sections', + backToApp: 'Back to app', + close: 'Close', + loading: 'Loading settings', + retry: 'Try again', + save: 'Save', + cancel: 'Cancel', + copy: 'Copy', + copied: 'Copied', + failed: 'Failed', + settingsLoadFailed: 'Could not load settings', + usageLoadFailed: 'Could not load usage statistics', + runtimeHost: 'Runtime Host', + runtimeHostUnavailable: 'This Runtime Host is unavailable. Choose another Host or retry the connection under Projects.', + unknownError: 'Something went wrong. Try again.', + unavailablePage: 'This page is part of the Maka settings tree and will activate with its runtime capability.', + showDetails: 'Show details', + hideDetails: 'Hide details', + ready: 'Ready', + groups: { + memorySources: 'Memory', + memorySourcesHelp: 'Maka remembers information you confirm in chat and uses it in later answers.', + memoryDocument: 'Memory file and backups', + memoryDocumentHelp: 'Memory lives in a local MEMORY.md; edit the raw file or restore a backup here.', + memoryEntries: 'What Maka remembers', + memoryEntriesHelp: 'Filter entries, add one manually, or archive what is no longer needed.', + searchProvider: 'Search provider', + searchProviderHelp: 'The provider and credentials web search uses.', + searchBehavior: 'Search behavior', + searchBehaviorHelp: 'When a search runs, and how many results it returns.', + 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.', + buildInfo: 'Build info', + reference: 'Reference', + }, + } } satisfies UiCatalog; export function getSettingsSharedCopy(locale: UiLocale): SettingsSharedCopy { diff --git a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts index a77ccfae23..4ac18bb64d 100644 --- a/apps/desktop/src/renderer/locales/settings-subagents-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-subagents-copy.ts @@ -346,6 +346,88 @@ const SETTINGS_SUBAGENTS_COPY_BY_LOCALE = { max: 'Maximum', }, }, + ko: { + section: { + title: 'Approved subagents', + 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.', + }, + row: { + enabled: 'Enabled', + 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', + }, + editor: { + backToList: 'Back to subagents', + createSubtitle: 'Create a model preset that the main agent can select automatically.', + editSubtitle: 'Change its usage guidance, capability boundary, and model route.', + groupPurpose: 'Purpose', + groupPurposeHelp: 'The main agent selects a preset primarily from the name and guidance here.', + groupRoute: 'Capability and model', + groupRouteHelp: 'Fix what this subagent may do, and which model it runs on.', + dangerZone: 'Remove subagent', + dangerZoneHelp: 'This cannot be undone.', + delete: 'Remove', + enabled: 'Enabled', + enabledDescription: 'Turn this off to keep the preset without letting the main agent select it.', + name: 'Display name', + namePlaceholder: 'Fast code reader', + id: 'subagent_id', + idDescription: 'Stable after creation. The main agent and task history use it to identify this preset.', + idPlaceholder: 'fast-reader', + description: 'When to use', + descriptionPlaceholder: 'Fast, low-cost exploration of large repositories', + profile: 'Capability profile', + connection: 'Model connection', + model: 'Model', + thinking: 'Thinking level', + defaultThinking: 'Use model default', + implementationWarning: 'The Implementation profile can write files and run commands inside an isolated worktree.', + 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.`, + 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…', + }, + remove: { + title: (name) => `Remove “${name}”?`, + description: 'The main agent will no longer see this preset. Existing child tasks are not deleted.', + confirm: 'Remove', + 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.', + }, + 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.' }, + }, + thinking: { + off: 'Off', + minimal: 'Minimal', + low: 'Low', + medium: 'Medium', + high: 'High', + xhigh: 'Extra high', + max: 'Maximum', + }, + } } satisfies UiCatalog; export function getSubagentSettingsCopy(locale: UiLocale): SubagentSettingsCopy { diff --git a/apps/desktop/src/renderer/locales/settings-tasks-copy.ts b/apps/desktop/src/renderer/locales/settings-tasks-copy.ts index d58505f2e6..9f7a1b3237 100644 --- a/apps/desktop/src/renderer/locales/settings-tasks-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-tasks-copy.ts @@ -149,6 +149,41 @@ const SETTINGS_TASKS_COPY_BY_LOCALE = { emptyTitle: 'Nothing archived', emptyBody: 'Archive a task from the rail to restore or permanently delete it here.', }, + ko: { + listAria: 'Archived tasks', + noProject: 'No project', + 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.', + 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.`, + 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.`, + 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.', + moreActions: (name: string) => `More actions for ${name}`, + restore: 'Restore', + restoreTask: (name: string) => `Restore ${name}`, + delete: 'Delete', + emptyTitle: 'Nothing archived', + emptyBody: 'Archive a task from the rail to restore or permanently delete it here.', + } } satisfies UiCatalog; export function getSettingsTasksCopy(locale: UiLocale): SettingsTasksCopy { 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 46c54c86d0..7535db0511 100644 --- a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts @@ -162,6 +162,36 @@ const COPY = { }, }, }, + ko: { + proxy: { + 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.", + 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.", + 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.", + }, + } } satisfies UiCatalog; export function settingsTestResultMessage( diff --git a/apps/desktop/src/renderer/locales/settings-usage-copy.ts b/apps/desktop/src/renderer/locales/settings-usage-copy.ts index 75cf3752ad..b867520236 100644 --- a/apps/desktop/src/renderer/locales/settings-usage-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-usage-copy.ts @@ -103,6 +103,28 @@ const SETTINGS_USAGE_COPY = { pricingEmptyBody: 'Without pricing overrides, costs use the built-in model pricing table. Add custom prices here for specific models.', }, }, + ko: { + 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', + 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', + 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.', + }, + } } satisfies UiCatalog; export function getUsageSettingsCopy(locale: UiLocale): UsageSettingsCopy { 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 0cd8730aff..e39e1b1a2f 100644 --- a/apps/desktop/src/renderer/locales/settings-web-search-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-web-search-copy.ts @@ -89,6 +89,23 @@ const SETTINGS_WEB_SEARCH_COPY = { 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.' }, }, + ko: { + 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 UiCatalog; 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 80407441a1..da4d4da120 100644 --- a/apps/desktop/src/renderer/locales/shell-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-copy.ts @@ -2277,6 +2277,597 @@ 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`, + bulkDeleteTitle: (count: number) => `Delete ${count} selected tasks?`, + bulkDeleteDescription: + 'This cannot be undone, and every revision of each task goes with it.', + bulkArchiveTitle: (count: number) => `Archive ${count} selected tasks?`, + bulkArchiveDescription: 'Archived tasks stay available under Settings › Activity.', + bulkArchiveLabel: 'Archive', + bulkDeletedTitle: (count: number) => `Deleted ${count} tasks`, + bulkArchivedTitle: (count: number) => `Archived ${count} tasks`, + bulkKeptRestored: (count: number) => `${count} were restored meanwhile and kept.`, + bulkDeleteFailedTitle: 'Some tasks were not deleted', + bulkArchiveFailedTitle: 'Some tasks were not archived', + bulkFailedBody: (count: number) => `${count} of them did not go through.`, + bulkUnverified: 'The outcome could not be confirmed. Refresh to see what remains.', + bulkDeleteSubtaskNote: () => 'Their ordinary subtasks will be kept and moved to Archived.', + bulkDeleteSubtaskNoteUncertain: () => + 'Any ordinary subtasks they have will be kept and moved to Archived.', + }, + 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: { + permissionLabels: { + ask: 'Auto', + bypass: 'Full access', + }, + permissionDescriptions: { + explore: 'Read only: reads and searches only; writing files and network access ask you first.', + ask: "Auto: runs inside Maka's protection layer and asks before anything goes beyond the current permissions.", + bypass: "Local tools reach your files and your network directly, outside Maka's protection layer.", + }, + 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', + permissionSwitched: (label: string) => `Switched to ${label}`, + permissionFailedTitle: 'Could not change permission mode', + permissionFallback: 'The permission mode could not be changed. Try again later.', + modelSwitchedTitle: 'Task model changed', + modelSwitchedDescription: (from, to) => `${from} → ${to}`, + modelFailedTitle: 'Could not change model', + modelFallback: 'The model could not be changed. Try again later.', + modelRecoveryHint: 'If the selected connection needs sign-in or an API key, complete it in Settings · Models and try again.', + thinkingUpdatedTitle: 'Thinking level updated', + thinkingDefault: 'Default', + thinkingLabels: { + off: 'Off', + minimal: 'Minimal', + low: 'Low', + medium: 'Medium', + high: 'High', + xhigh: 'Extra high', + max: 'Maximum', + }, + 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', + descriptionBeforeRetry: 'An unhandled React error was caught. The summary is below. Choose', + retry: 'Try again', + descriptionBeforeReload: 'to clear this crash, or', + reload: 'Reload', + descriptionAfterReload: 'to refresh the entire window. Copy the diagnostics before handing off the issue.', + errorDetails: 'Error details', + componentStack: 'Component stack', + clipboardFailure: 'The clipboard is unavailable or was denied. You can select the error summary above manually.', + }, + 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…', + permissionModeChanging: 'The permission mode is changing. Wait for it to finish before continuing.', + 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 17fb805908..abf120b1ac 100644 --- a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts +++ b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts @@ -255,6 +255,7 @@ const COPY = { 'zh-CN': zhCopy, 'zh-TW': zhTwCopy, en: enCopy, + ko: enCopy, } satisfies UiCatalog; export function getShellRemainingCopy(locale: UiLocale): ShellRemainingCopy { diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index 744a62fdbb..e2aa8840c9 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -51,9 +51,10 @@ export const UNKNOWN_PROVIDER_DESCRIPTION = { 'zh-CN': '该 provider 在当前版本未注册。', 'zh-TW': '該 provider 在目前版本未註冊。', en: 'This provider is not registered in the current build.', + ko: 'This provider is not registered in the current build.', } satisfies UiCatalog; -export const PROVIDER_DISPLAY_COPY = { +const RAW_PROVIDER_DISPLAY_COPY = { 'kimi-coding-plan': { 'zh-CN': { name: 'Kimi Coding Plan', description: '月之暗面 · Anthropic 兼容', badge: 'Coding' }, 'zh-TW': { name: 'Kimi Coding Plan', description: '月之暗面 · Anthropic 相容', badge: 'Coding' }, @@ -365,7 +366,14 @@ export const PROVIDER_DISPLAY_COPY = { 'zh-TW': { name: 'xAI OAuth', description: '使用 SuperGrok 或 X Premium 帳號登入。', badge: 'Account' }, en: { name: 'xAI OAuth', description: 'Sign in with SuperGrok or X Premium.', badge: 'Account' }, }, -} satisfies Record>; +} satisfies Record, 'ko'>>; + +export const PROVIDER_DISPLAY_COPY = Object.fromEntries( + Object.entries(RAW_PROVIDER_DISPLAY_COPY).map(([type, catalog]) => [ + type, + { ...catalog, ko: catalog.en }, + ]), +) as Record>; export function providerDisplay(type: ProviderType, locale: UiLocale): ProviderCopy { const copy = (PROVIDER_DISPLAY_COPY as Partial>>)[type]?.[locale]; diff --git a/packages/cli/src/__tests__/tui-copy-catalog.test.ts b/packages/cli/src/__tests__/tui-copy-catalog.test.ts index d1bf2525ea..9eb9a12e2e 100644 --- a/packages/cli/src/__tests__/tui-copy-catalog.test.ts +++ b/packages/cli/src/__tests__/tui-copy-catalog.test.ts @@ -19,7 +19,7 @@ import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; -import { formatUiMessage, UI_LOCALES } from '@maka/core/ui-locale'; +import { formatUiMessage, UI_LOCALES, resolveUiMessageCatalog } from '@maka/core/ui-locale'; import { getTuiPickerCopy, onboardingFailureMessage } from '../pi-tui-pickers.js'; import { TUI_COPY_RESOURCES } from '../tui-copy-catalog.js'; @@ -56,7 +56,13 @@ const MESSAGE_VALUES = { describe('TUI copy resources', () => { test('registers every domain without a locale-specific getter branch', () => { for (const [domain, catalog] of Object.entries(TUI_COPY_RESOURCES)) { - for (const locale of UI_LOCALES) assert.ok(catalog[locale], `${domain}/${locale}`); + for (const locale of ['en', 'zh-CN', 'zh-TW'] as const) { + assert.ok(catalog[locale], `${domain}/${locale}`); + } + assert.ok( + resolveUiMessageCatalog(catalog as never).ko, + `${domain}/ko`, + ); } }); diff --git a/packages/core/src/redaction.ts b/packages/core/src/redaction.ts index 54c5644c6a..fef441ccd2 100644 --- a/packages/core/src/redaction.ts +++ b/packages/core/src/redaction.ts @@ -278,6 +278,13 @@ export const GENERALIZED_ERROR_COPY = { provider_error: 'Provider returned an error', network_error: 'Network error', }, + ko: { + timeout: '요청 시간 초과', + rate_limited: '모델 속도 제한 초과', + auth_failed: '인증 실패', + provider_error: '모델 서비스 오류', + network_error: '네트워크 오류', + }, } satisfies UiCatalog>; export function generalizedErrorMessageForLocale( diff --git a/packages/core/src/relative-time.ts b/packages/core/src/relative-time.ts index e2d41d4fdd..52f1c66185 100644 --- a/packages/core/src/relative-time.ts +++ b/packages/core/src/relative-time.ts @@ -51,6 +51,7 @@ const JUST_NOW: UiCatalog = { 'zh-CN': '刚刚', 'zh-TW': '剛剛', 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 760d8990d4..02e591942c 100644 --- a/packages/core/src/tool-quiet-preview.ts +++ b/packages/core/src/tool-quiet-preview.ts @@ -82,6 +82,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}개` : ''), + }, }; function strings(locale: UiLocale): QuietPreviewStrings { diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 9342f8a06f..24ff7fd280 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -965,6 +965,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', projects: 'Projects', 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}`, selectRow: 'Select', selectionBarAriaLabel: 'Bulk actions for selected tasks', selectedCount: (selected, total) => `${selected} / ${total} selected`, selectAllAriaLabel: 'Select all or none', selectionArchive: 'Archive', selectionDelete: 'Delete', selectionClear: 'Done', + }, + } } 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 298ba3167b..8f10e907c5 100644 --- a/packages/ui/src/daily-review-copy.ts +++ b/packages/ui/src/daily-review-copy.ts @@ -209,6 +209,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 00a6045497..38e0a72306 100644 --- a/packages/ui/src/locale-helpers.ts +++ b/packages/ui/src/locale-helpers.ts @@ -94,6 +94,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: 'Summarize codebase', prompt: 'Help me map this codebase: directory layout, key modules, and how they fit together.' }, + { label: 'Explain code', prompt: 'Paste a snippet — explain it line by line and flag any pitfalls:\n\n```\n\n```' }, + { label: 'Read a long doc', prompt: 'Here\'s an article or doc — pull out the core argument, list the key facts, and tell me what I might be missing:\n\n' }, + { label: 'Translate & polish', prompt: 'Translate the text below into Chinese; keep the meaning, tone should stay natural and professional:\n\n' }, + { 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```' }, + ], }; export function getPromptSuggestions(locale: UiLocale): PromptSuggestion[] { diff --git a/packages/ui/src/scheduled-task-copy.ts b/packages/ui/src/scheduled-task-copy.ts index 1b3ae7e74f..d4cb1c8e7d 100644 --- a/packages/ui/src/scheduled-task-copy.ts +++ b/packages/ui/src/scheduled-task-copy.ts @@ -276,6 +276,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 ffb0522343..5d1178fe26 100644 --- a/packages/ui/src/session-hover-card-copy.ts +++ b/packages/ui/src/session-hover-card-copy.ts @@ -69,6 +69,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 9784398d2f..d389c19fe2 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -344,6 +344,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 0fa9a18702..2fc608a758 100644 --- a/packages/ui/src/shell-controls-copy.ts +++ b/packages/ui/src/shell-controls-copy.ts @@ -144,6 +144,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 8385e95e73..929111ef2b 100644 --- a/packages/ui/src/skills-copy.ts +++ b/packages/ui/src/skills-copy.ts @@ -197,6 +197,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 8b042f8a71..26e0a40920 100644 --- a/packages/ui/src/tool-activity/copy.ts +++ b/packages/ui/src/tool-activity/copy.ts @@ -503,6 +503,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 { diff --git a/scripts/add-ko-catalog-stub.mjs b/scripts/add-ko-catalog-stub.mjs new file mode 100644 index 0000000000..bfa8f5a53b --- /dev/null +++ b/scripts/add-ko-catalog-stub.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Adds `ko:` entries to UiCatalog objects by duplicating the `en:` block. + * Used for integration branches where ko is enabled before all slices land. + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + +const files = [ + 'packages/ui/src/tool-activity/copy.ts', + 'packages/ui/src/skills-copy.ts', + 'packages/ui/src/shell-controls-copy.ts', + 'packages/ui/src/shared-ui-copy.ts', + 'packages/ui/src/scheduled-task-copy.ts', + 'packages/ui/src/daily-review-copy.ts', + 'packages/ui/src/conversation-copy.ts', + 'apps/desktop/src/renderer/settings/provider-display-copy.ts', + 'apps/desktop/src/renderer/locales/shell-remaining-copy.ts', + 'apps/desktop/src/renderer/locales/shell-copy.ts', + 'apps/desktop/src/renderer/locales/settings-web-search-copy.ts', + 'apps/desktop/src/renderer/locales/settings-usage-copy.ts', + 'apps/desktop/src/renderer/locales/settings-test-result-copy.ts', + 'apps/desktop/src/renderer/locales/settings-tasks-copy.ts', + 'apps/desktop/src/renderer/locales/settings-subagents-copy.ts', + 'apps/desktop/src/renderer/locales/settings-shared-copy.ts', + 'apps/desktop/src/renderer/locales/settings-provider-copy.ts', + 'apps/desktop/src/renderer/locales/settings-projects-copy.ts', + 'apps/desktop/src/renderer/locales/settings-preferences-copy.ts', + 'apps/desktop/src/renderer/locales/settings-navigation-copy.ts', + 'apps/desktop/src/renderer/locales/settings-memory-copy.ts', + 'apps/desktop/src/renderer/locales/settings-health-copy.ts', + 'apps/desktop/src/renderer/locales/settings-data-copy.ts', + 'apps/desktop/src/renderer/locales/settings-daily-review-copy.ts', + 'apps/desktop/src/renderer/locales/settings-bot-copy.ts', + 'apps/desktop/src/renderer/locales/session-collaboration-copy.ts', + 'apps/desktop/src/renderer/locales/plan-mode-copy.ts', + 'apps/desktop/src/renderer/locales/permission-center-copy.ts', + 'apps/desktop/src/renderer/locales/mcp-copy.ts', + 'apps/desktop/src/renderer/locales/external-session-import-copy.ts', + 'apps/desktop/src/renderer/locales/conversation-copy.ts', + 'apps/desktop/src/renderer/locales/browser-copy.ts', + 'apps/desktop/src/renderer/locales/artifact-copy.ts', + 'apps/desktop/src/main/client-settings-confirmation-copy.ts', + 'apps/desktop/src/main/permission-overlay/permission-overlay-copy.ts', + 'apps/desktop/src/main/runtime-host-upgrade-copy.ts', +]; + +function extractLocaleBlock(source, localeKey) { + const marker = `\n ${localeKey}: `; + const start = source.indexOf(marker); + if (start === -1) return null; + let i = start + marker.length; + if (source[i] !== '{') return null; + let depth = 0; + const begin = i; + for (; i < source.length; i++) { + const ch = source[i]; + if (ch === '{') depth++; + else if (ch === '}') { + depth--; + if (depth === 0) { + return source.slice(start, i + 1); + } + } + } + return null; +} + +for (const file of files) { + const path = join(root, file); + let source = readFileSync(path, 'utf8'); + if (source.includes('\n ko:') || source.includes("\n 'ko':")) { + continue; + } + const enBlock = extractLocaleBlock(source, 'en'); + if (!enBlock) { + console.warn(`skip (no en block): ${file}`); + continue; + } + const koBlock = enBlock.replace('\n en:', '\n ko:'); + const insertAt = source.lastIndexOf('\n} satisfies UiCatalog'); + if (insertAt === -1) { + console.warn(`skip (no satisfies): ${file}`); + continue; + } + source = `${source.slice(0, insertAt)},${koBlock}${source.slice(insertAt)}`; + writeFileSync(path, source); + console.log(`updated: ${file}`); +} From 27e7dec63215cac40567c108c1a8a3ec0802156c Mon Sep 17 00:00:00 2001 From: Changsu Seong <110822847+scs0209@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:10:05 +0900 Subject: [PATCH 3/4] fix(i18n-ko): add Korean copy for session collaboration Replace the ko: EN stub with a dedicated KO translation block. Generated-by: Cursor Co-authored-by: Cursor --- .../locales/session-collaboration-copy.ts | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts index b096ebfa15..4f79dd6339 100644 --- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts +++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts @@ -346,7 +346,77 @@ const EN = { pendingTurnRequestCount: (count: number) => `${count} pending Turn ${count === 1 ? 'request' : 'requests'}`, } satisfies SessionCollaborationCopy; -const COPY = { 'zh-CN': ZH_CN, 'zh-TW': ZH_TW, en: EN, ko: EN } satisfies UiCatalog; +const KO = { + shareAction: '이 작업 공유', + shareTitle: '작업 공유', + shareDescription: '다른 Maka 설치에 일회용 초대를 만듭니다.', + enableRemoteAccessTitle: '먼저 원격 액세스를 켜세요', + enableRemoteAccessBody: + 'Runtime Host 설정이 열렸습니다. 원격 액세스를 켠 뒤 이 작업을 공유하세요.', + disclosureTitle: '공유 전 확인', + disclosureBody: + '게스트는 이 작업의 기존 및 이후에 생성되는 모든 표시 가능한 콘텐츠(파일 경로, 자격 증명, 기타 비밀 포함)를 볼 수 있습니다. 액세스를 취소해도 이후 읽기만 막을 수 있으며, 이미 복사한 내용은 회수할 수 없습니다.', + accessLabel: '액세스', + observe: '읽기 전용', + observeHelp: '전체 기록과 실시간 업데이트 보기', + requestTurn: '턴 요청 가능', + revokeTurnRequests: '턴 요청 권한 취소', + requestTurnHelp: '작업 전체를 보고, 건별 승인이 필요한 새 턴을 요청', + createInvitation: '초대 만들기', + invitationCode: '일회용 초대 코드', + invitationHelp: '코드에는 연결 주소와 게스트 자격 증명이 포함되며, 소유자 자격 증명은 포함되지 않습니다.', + copy: '초대 복사', + copied: '초대가 복사됨', + close: '완료', + activeAccess: '현재 액세스', + accessUnavailable: '공유 제어를 일시적으로 사용할 수 없습니다. 이 창이 자동으로 다시 시도합니다.', + guest: '게스트', + noAccess: '아직 액세스 권한이 있는 사람이 없습니다', + pending: '대기 중', + active: '연결됨', + revoke: '취소', + joinAction: '공유 작업 참가', + joinTitle: '공유 작업 참가', + joinDescription: '초대를 붙여넣어 독립적인 게스트 연결을 만듭니다.', + code: '초대 코드', + join: '참가', + validatingInvitation: '초대를 확인하는 중…', + discoveringHost: '이 작업의 Runtime Host를 찾는 중…', + preparingRoute: '사용 가능한 경로를 준비하는 중…', + connectingHost: 'Runtime Host에 연결하는 중…', + authenticatingGuest: '게스트 자격 증명을 확인하는 중…', + finalizingAccess: '게스트 액세스를 확인하는 중…', + loadingSession: '공유 작업을 불러오는 중…', + invalidCode: '초대 코드가 유효하지 않습니다', + connectionFailed: '공유 작업에 참가할 수 없습니다', + directPathUnavailable: + '이 작업의 Runtime Host에 연결할 수 없습니다. 이 Desktop Client와 Host가 같은 Peer Mesh에 있고, 직접 연결 또는 멤버 전달 경로가 있는지 확인하세요. Peer Mesh 설정에서 경로를 동기화하고 멤버 전달을 확인하세요.', + insecureTitle: '이 연결은 암호화되지 않았습니다', + insecureBody: + '게스트 자격 증명, 작업 전체 내용, 턴 요청이 같은 네트워크의 다른 사용자에게 가로채일 수 있습니다. 위험을 이해하고 감수할 때만 계속하세요.', + shareInsecure: '위험을 감수하고 만들기', + joinInsecure: '위험을 감수하고 참가', + retainedTasks: '참가한 공유 작업', + disconnect: '연결 해제', + disconnectFailed: '공유 작업 연결을 해제할 수 없습니다', + turnRequests: '턴 요청', + noTurnRequests: '턴 요청 없음', + approve: '승인', + reject: '거부', + turnRequestPlaceholder: '시작하려는 새 턴을 설명하세요', + submitTurnRequest: '새 턴 요청', + turnRequestSent: '요청이 전송되었습니다. 소유자 승인을 기다리는 중', + turnRequestReconciling: 'Host가 이 요청을 받았는지 확인하는 중…', + turnRequestPending: '승인 대기 중', + turnRequestApproved: '승인됨', + turnRequestRejected: '거부됨', + turnRequestStarted: '시작됨', + turnRequestBlocked: '시작할 수 없음', + turnRequestFailed: '승인 실패', + dismissTurnRequest: '닫기', +} satisfies SessionCollaborationCopy; + +const COPY = { 'zh-CN': ZH_CN, 'zh-TW': ZH_TW, en: EN, ko: KO } satisfies UiCatalog; export function getSessionCollaborationCopy(locale: UiLocale) { return COPY[locale]; From 9af408a080ab80aa50f83c439762ddd8ac061d1d Mon Sep 17 00:00:00 2001 From: Changsu Seong <110822847+scs0209@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:54:37 +0900 Subject: [PATCH 4/4] fix(i18n-ko): resolve upstream rebase conflicts for Korean locale - rebase Korean locale commits onto latest upstream/main - restore catalogs broken by stub insertion and re-add ko stubs - keep @maka/ui and @maka/core builds green after conflict cleanup Co-authored-by: Cursor --- .../main/client-settings-confirmation-copy.ts | 10 +- apps/desktop/src/main/notifications-policy.ts | 4 +- .../src/main/runtime-host-boot-copy.ts | 16 +- .../app-update/locales/app-update-copy.ts | 11 ++ .../src/renderer/locales/agent-graph-copy.ts | 46 +++++ .../src/renderer/locales/peer-mesh-copy.ts | 162 ++++++++++++++++++ .../renderer/locales/session-local-copy.ts | 10 ++ .../src/renderer/locales/settings-bot-copy.ts | 7 + .../renderer/locales/settings-health-copy.ts | 17 +- .../locales/settings-navigation-copy.ts | 2 +- .../renderer/locales/settings-shared-copy.ts | 2 - .../locales/settings-test-result-copy.ts | 12 ++ .../renderer/locales/task-readiness-copy.ts | 12 ++ .../src/renderer/locales/workhub-copy.ts | 13 ++ apps/desktop/src/renderer/mcp-catalog.ts | 16 ++ packages/core/src/redaction.ts | 10 +- .../src/__tests__/search-modal-source.test.ts | 1 + .../tool-activity-presentation.test.ts | 1 + packages/ui/src/astryx-i18n.tsx | 6 + packages/ui/src/conversation-copy.ts | 39 ++++- packages/ui/src/runtime-resume-copy.ts | 43 +++++ packages/ui/src/session-hover-card-copy.ts | 21 +-- packages/ui/src/shared-ui-copy.ts | 7 - packages/ui/src/shell-controls-copy.ts | 15 +- packages/ui/src/skills-copy.ts | 2 +- packages/ui/src/tool-activity/copy.ts | 4 + 26 files changed, 429 insertions(+), 60 deletions(-) diff --git a/apps/desktop/src/main/client-settings-confirmation-copy.ts b/apps/desktop/src/main/client-settings-confirmation-copy.ts index 068a655fd0..7b22542242 100644 --- a/apps/desktop/src/main/client-settings-confirmation-copy.ts +++ b/apps/desktop/src/main/client-settings-confirmation-copy.ts @@ -51,11 +51,11 @@ const COPY = { buttons: ['Apply changes', 'Cancel'], }, ko: { - labels: { theme: '테마', palette: '팔레트', uiLocale: 'UI 언어', runComplete: '응답 완료 알림', keepSystemAwake: '시스템 깨어 있음 유지' }, - on: '켜짐', - off: '꺼짐', - message: 'Maka가 이 클라이언트 설정을 업데이트하도록 허용할까요?', - buttons: ['변경 적용', '취소'], + labels: { theme: 'Theme', palette: 'Palette', uiLocale: 'UI language', runComplete: 'Run-complete notifications', keepSystemAwake: 'Keep system awake' }, + on: 'true', + off: 'false', + message: "Allow Maka to update this client's settings?", + buttons: ['Apply changes', 'Cancel'], }, } satisfies UiCatalog; diff --git a/apps/desktop/src/main/notifications-policy.ts b/apps/desktop/src/main/notifications-policy.ts index e7bd5f6579..6668ae32ba 100644 --- a/apps/desktop/src/main/notifications-policy.ts +++ b/apps/desktop/src/main/notifications-policy.ts @@ -91,8 +91,8 @@ const RUN_NOTIFICATION_COPY = { completed: { title: 'Response ready', body: 'Maka finished this response. Click to view it.' }, }, ko: { - errored: { title: '대화 오류', body: '이 응답이 완료되지 않았습니다. 클릭하여 자세히 보세요.' }, - completed: { title: '응답 준비됨', body: 'Maka가 이 응답을 완료했습니다. 클릭하여 확인하세요.' }, + errored: { title: 'Conversation error', body: 'This response did not finish. Click to view details.' }, + completed: { title: 'Response ready', body: 'Maka finished this response. Click to view it.' }, }, } satisfies UiCatalog>; diff --git a/apps/desktop/src/main/runtime-host-boot-copy.ts b/apps/desktop/src/main/runtime-host-boot-copy.ts index d60474c84c..6b3a52dfa9 100644 --- a/apps/desktop/src/main/runtime-host-boot-copy.ts +++ b/apps/desktop/src/main/runtime-host-boot-copy.ts @@ -85,18 +85,18 @@ const STARTUP_RECOVERY_COPY = { }, ko: { storageRoot: { - title: 'Maka 작업 공간을 복구해야 합니다', - message: 'Maka가 이 작업 공간을 확인할 수 없습니다.', + title: 'Maka workspace needs repair', + message: 'Maka cannot verify this workspace.', detail: (workspaceRoot) => - `시스템의 디스크 식별자가 변경되었을 수 있습니다. 원본 Maka 작업 공간인 경우에만 복구하세요. 복사된 작업 공간이면 복구하지 마세요.\n\n${workspaceRoot}`, - buttons: ['작업 공간 복구', '종료'], + `The disk identity may have changed. Repair only if this is the original Maka workspace on this computer, not a copied workspace.\n\n${workspaceRoot}`, + buttons: ['Repair Workspace', 'Exit'], }, runtimeHost: { - title: '기본 Runtime Host에 연결할 수 없습니다', - message: (profileName) => `${profileName}에 연결할 수 없습니다`, + title: 'Default Runtime Host is unavailable', + message: (profileName) => `Could not connect to ${profileName}`, detail: (message) => - `${message}\n\n다시 시도하거나 Local을 기본 Host로 사용하거나, 현재 선택을 유지한 뒤 나중에 설정에서 처리할 수 있습니다.`, - buttons: ['다시 시도', 'Local 사용', '오프라인 유지'], + `${message}\n\nRetry, use Local as the default Host, or keep the current selection and resolve it later in Settings.`, + buttons: ['Retry', 'Use Local', 'Keep Offline'], }, }, } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/features/app-update/locales/app-update-copy.ts b/apps/desktop/src/renderer/features/app-update/locales/app-update-copy.ts index 695ab79621..4c4e5cd5cc 100644 --- a/apps/desktop/src/renderer/features/app-update/locales/app-update-copy.ts +++ b/apps/desktop/src/renderer/features/app-update/locales/app-update-copy.ts @@ -65,6 +65,17 @@ const COPY_BY_LOCALE = { retryFailedTitle: 'Could not retry update download', retryFailedFallback: 'Try again later, or download the latest version manually.', }, + ko: { + installFailedTitle: 'Could not install update', + installFailedFallback: 'Try again later.', + installManualFallback: 'Try again later, or download the latest version manually.', + activeTasksTitle: 'Tasks are still running', + activeTasksDescription: 'Tasks are still running. Updating will interrupt them. Continue?', + activeTasksConfirm: 'Update anyway', + activeTasksCancel: 'Cancel', + retryFailedTitle: 'Could not retry update download', + retryFailedFallback: 'Try again later, or download the latest version manually.', + }, } satisfies UiCatalog; export function getAppUpdateCopy(locale: UiLocale): AppUpdateCopy { diff --git a/apps/desktop/src/renderer/locales/agent-graph-copy.ts b/apps/desktop/src/renderer/locales/agent-graph-copy.ts index e87370a159..ffb4073e5c 100644 --- a/apps/desktop/src/renderer/locales/agent-graph-copy.ts +++ b/apps/desktop/src/renderer/locales/agent-graph-copy.ts @@ -188,6 +188,52 @@ const AGENT_GRAPH_PANEL_COPY = { })[status], wait: waitReasonEn, }, + ko: { + title: 'Agent Graph', + loading: 'Loading graph state…', + retry: 'Retry', + collapse: 'Collapse Agent Graph', + expand: 'Expand Agent Graph', + dismiss: 'Dismiss Agent Graph', + stop: 'Stop graph', + stopping: 'Stopping…', + stopFailed: 'Could not stop the graph. Try again.', + loadFailed: 'Could not refresh graph state.', + openSession: 'Open child task', + operators: 'Operators', + selectedResults: 'Selected results', + epoch: 'Graph run', + currentEpoch: 'Current', + historicalEpoch: 'History (read-only)', + cappedEpochs: (count) => `Showing the newest ${count} runs`, + noOperators: 'Waiting for the main agent to create an operator…', + hiddenOperators: (count) => `${count} more operator${count === 1 ? '' : 's'}`, + progress: (settled, total, hasOmitted) => + hasOmitted ? `${settled}/${total} visible settled` : `${settled}/${total} settled`, + status: (status) => + ({ + empty: 'Awaiting schedule', + active: 'Running', + closing: 'Finishing', + waiting: 'Waiting', + stopped: 'Stopped', + failed: 'Failed', + completed: 'Completed', + })[status], + operatorStatus: (status) => + ({ + not_started: 'Not started', + waiting: 'Waiting', + runnable: 'Runnable', + running: 'Running', + blocked: 'Blocked', + completed: 'Completed', + failed: 'Failed', + aborted: 'Aborted', + cancelled: 'Cancelled', + })[status], + wait: waitReasonEn, + }, } satisfies UiCatalog; export function getAgentGraphPanelCopy(locale: UiLocale): AgentGraphPanelCopy { diff --git a/apps/desktop/src/renderer/locales/peer-mesh-copy.ts b/apps/desktop/src/renderer/locales/peer-mesh-copy.ts index c9fa53ee34..4f6e93bac1 100644 --- a/apps/desktop/src/renderer/locales/peer-mesh-copy.ts +++ b/apps/desktop/src/renderer/locales/peer-mesh-copy.ts @@ -626,6 +626,168 @@ const PEER_MESH_COPY = { localHostMissingHint: 'Add it so other members can reach tasks shared from this device through the Mesh.', }, + ko: { + title: 'Peer Mesh', + experimental: 'Experimental', + failed: 'Peer Mesh operation failed', + invalidResult: 'Peer Mesh returned an invalid result', + unknownError: 'Peer Mesh operation failed', + outcomeUnknown: + 'The Host may have completed this operation. Its current state was refreshed; review it before trying again.', + invitationOutcomeUnknown: + 'The Host may have created an invitation, but its one-time code was not returned and cannot be recovered. It will expire automatically; review the pending invitation count before creating another.', + outcomeUnknownRefreshFailed: + 'The Host may have completed this operation, but its current state could not be refreshed. Reconnect and refresh before trying again.', + unavailable: 'Peer Mesh is unavailable for this endpoint', + loading: 'Loading Mesh status…', + checkingPeerConnection: "Checking this Runtime Host's peer connection…", + peerConnectionDisabled: 'Peer connectivity is not enabled for this Runtime Host', + peerConnectionDisabledHint: + 'Enable it so this Host can create or join Meshes. The existing SSH connection remains available.', + peerConnectionDisableProfileFirst: + "This Host's Direct peer connection is in use. Disable it in the Host list before changing the listener.", + enablePeerConnection: 'Enable peer connectivity', + peerConnectionStarting: 'Peer connectivity is enabled; the Mesh endpoint is starting', + peerConnectionStartingHint: 'This normally takes a few seconds. You can also check again.', + peerConnectionUpgradeRequired: 'This Runtime Host version cannot manage Peer Mesh', + peerConnectionUpgradeRequiredHint: 'Update this Host before enabling peer connectivity.', + working: { + refresh: 'Refreshing Peer Mesh…', + create: 'Creating Mesh…', + join: 'Joining Mesh…', + invite: 'Preparing invitation…', + 'add-host': 'Adding the Runtime Host to the Mesh…', + 'enable-peer': 'Enabling peer connectivity for the Runtime Host…', + update: 'Updating Mesh…', + rename: 'Saving name…', + }, + settling: 'Confirming the final state…', + endpoint: 'Manage endpoint', + desktopEndpoint: 'Desktop Client', + hostEndpoint: 'Local Runtime Host', + desktopEndpointHelp: 'This Client connects to Runtime Hosts in the Mesh.', + hostEndpointHelp: 'Add this Host so other members can reach tasks shared from this device.', + advancedSettings: 'Advanced settings', + technicalDetails: 'Identity and connectivity details', + connectivityAutomatic: 'Automatic connectivity (recommended)', + connectivityKnownRoutesOnly: 'Known routes only', + connectivityCustom: 'Custom address discovery', + restartRequired: 'Restart required', + adaptiveConnectivity: 'Adaptive connectivity', + adaptiveConnectivityHelp: + 'Maka races available direct paths automatically and uses approved member transit when needed. You do not choose a transport protocol here.', + connectivityPolicyLoading: 'Loading connectivity policy…', + connectivityPolicyLoadFailed: 'Could not load connectivity policy', + connectivityPolicySaveFailed: 'Could not save connectivity policy', + restoreDefaultConnectivityPolicy: 'Restore defaults', + connectivityPolicyRestartRequired: + 'Restart Maka to apply saved connectivity-policy changes to new connections.', + publicAddressDiscovery: 'Public address discovery', + publicStunDefault: 'Public STUN (recommended)', + publicStunDisabled: 'No public STUN', + publicStunCustom: 'Custom STUN', + customStunUrls: 'STUN addresses', + customStunUrlsInvalid: + 'Enter up to 8 comma-separated stun:host[:port] addresses.', + publicStunDefaultHelp: + 'Uses Cloudflare public STUN on a best-effort basis to discover public mappings. It never carries Maka traffic, but the provider can observe source IPs and request timing; Maka provides no availability guarantee.', + publicStunDisabledHelp: + 'Only local addresses and other known direct paths are attempted; direct connectivity across NAT may be reduced.', + publicStunCustomHelp: + 'Enter comma-separated stun: addresses. STUN discovers network addresses and never carries Session content.', + saveConnectivityPolicy: 'Save changes', + peerId: 'Peer ID', + peerIdHelp: 'Technical identity for this endpoint. Select the ID to copy its full value.', + meshId: 'Mesh ID', + meshIdHelp: 'Used to identify and diagnose this Mesh. Select the ID to copy its full value.', + thisRuntimeHost: 'This Runtime Host', + thisDesktop: 'This Desktop', + displayName: 'Name shown in the Mesh', + meshDisplayName: 'Mesh name', + unnamedMesh: 'Unnamed Mesh', + rename: 'Rename', + renameMesh: 'Rename Mesh', + save: 'Save', + peerIdCopied: 'Peer ID copied', + meshIdCopied: 'Mesh ID copied', + copyPeerId: (value: string) => `Copy full Peer ID: ${value}`, + copyMeshId: (value: string) => `Copy full Mesh ID: ${value}`, + empty: 'Build your first Mesh', + emptyHint: 'Create a new Mesh or join one with a one-time invitation.', + meshes: 'Meshes', + mesh: 'Mesh', + members: 'Members', + activeMeshCount: (value: number) => `${value} active`, + showClosedMeshes: (value: number) => `Show closed (${value})`, + noActiveMeshes: 'No active Meshes', + noActiveMeshesHint: 'Closed Meshes are hidden by default. Use the filter above to show them.', + authority: 'Owner', + member: 'Member', + closed: 'Closed', + memberCount: (value: number) => value === 1 ? '1 member' : `${value} members`, + pending: (value: number) => `${value} pending invites`, + transit: 'Member transit', + transitHelp: 'Let members of this Mesh connect through this device using its bandwidth.', + transitToggle: 'Provide transit for this Mesh', + transitStatus: 'Member transit status', + transitLimitsLabel: 'Member transit limits', + transitLimits: (value: PeerMeshQueryResult['transit']) => + value + ? `Fixed limits: ${value.maxCircuitsPerPeer} circuits per member, ${formatHours(value.maxCircuitDurationSeconds)} per circuit, and ${formatMebibytes(value.maxCircuitBytes)}. Only one Mesh can be served at a time.` + : 'Member transit uses fixed resource limits. Only one Mesh can be served at a time.', + allowedMembers: 'Allowed members', + reservations: 'Reservations', + circuits: 'Circuits', + routeState: { + local: 'Local', + connecting: 'Connecting', + reachable: 'Reachable', + reconnecting: 'Reconnecting', + needs_repair: 'Needs a new invitation', + }, + endpointKind: { + client: 'Client', + host: 'Runtime Host', + unknown: 'Unidentified peer', + }, + endpointKindHelp: { + client: 'A Client is the interface that connects to Hosts, browses tasks, and starts actions. It does not own tasks.', + host: 'A Runtime Host owns tasks and runtime state, and executes authorized work.', + unknown: 'This peer has not reported whether it is a Client or Runtime Host, usually because it uses an older build.', + }, + joinTitle: 'Join a Mesh', + joinHint: 'Paste a one-time invitation created by another peer.', + joinCode: 'Invitation', + join: 'Join', + joinMesh: 'Join Mesh', + invite: 'Invite member', + invitationTitle: 'Invite a member', + invitationFor: (value: string) => `Mesh ${value}`, + invitationWarning: + 'This code works once. Anyone holding it can admit one peer to this Mesh.', + invitationDirectOnly: + 'No coordination peer is available yet. This invitation contains direct routes only and may not work across NATs.', + invitationExpires: (value: string) => `Expires ${value}`, + invitationCopied: 'Invitation copied', + copyInvitation: 'Copy invitation', + create: 'Create Mesh', + refresh: 'Refresh', + back: 'Back', + leave: 'Leave Mesh', + closeMesh: 'Close Mesh', + remove: 'Remove member', + cancel: 'Cancel', + closeConfirm: 'Close this Mesh?', + leaveConfirm: 'Leave this Mesh?', + removeConfirm: 'Remove this member?', + meshActions: 'Mesh actions', + memberActions: (peerId: string) => `Actions for ${peerId}`, + addLocalHost: 'Add local Runtime Host', + localRuntimeHost: 'Runtime Host', + localHostMissing: 'Local Runtime Host has not joined', + localHostMissingHint: + 'Add it so other members can reach tasks shared from this device through the Mesh.', + }, } satisfies UiCatalog; export function getPeerMeshCopy(locale: UiLocale): PeerMeshCopy { diff --git a/apps/desktop/src/renderer/locales/session-local-copy.ts b/apps/desktop/src/renderer/locales/session-local-copy.ts index c96e9ff98b..abb315bf9e 100644 --- a/apps/desktop/src/renderer/locales/session-local-copy.ts +++ b/apps/desktop/src/renderer/locales/session-local-copy.ts @@ -41,6 +41,16 @@ const catalog = { check: 'Check status', updateError: 'Unable to update the saved message', }, + ko: { + saved: 'Saved locally · waiting to send', + sending: 'Delivering to Host', + accepted: 'Host accepted', + unknown: 'Host outcome unknown', + failed: 'Not sent · local copy retained', + remove: 'Remove local copy', + check: 'Check status', + updateError: 'Unable to update the saved message', + }, 'zh-CN': { saved: '已本地保存 · 等待发送', sending: '正在投递到 Host', diff --git a/apps/desktop/src/renderer/locales/settings-bot-copy.ts b/apps/desktop/src/renderer/locales/settings-bot-copy.ts index a9d245382d..7401df24fe 100644 --- a/apps/desktop/src/renderer/locales/settings-bot-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-bot-copy.ts @@ -55,6 +55,13 @@ const BOT_TRANSPORT_ERRORS = { provider_error: 'The platform is temporarily unavailable. Try again later', network_error: 'Network error. Check the network and proxy settings', }, + ko: { + timeout: 'Request timed out. Try again later', + rate_limited: 'Too many requests. Try again later', + auth_failed: 'Authentication failed. Check the credentials', + provider_error: 'The platform is temporarily unavailable. Try again later', + network_error: 'Network error. Check the network and proxy settings', + }, } satisfies UiCatalog>; const zhCopy = { diff --git a/apps/desktop/src/renderer/locales/settings-health-copy.ts b/apps/desktop/src/renderer/locales/settings-health-copy.ts index 1ae614eca8..3a1621b2d5 100644 --- a/apps/desktop/src/renderer/locales/settings-health-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-health-copy.ts @@ -175,12 +175,12 @@ const SETTINGS_HEALTH_COPY = { 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' }, + scopes: { llm_connection: 'LLM connection', bot: 'Bot', capability: 'Capability' }, + sources: { connection_test: 'Connection test', capability_snapshot: 'Capability snapshot', permission_snapshot: 'Permission snapshot', runtime_probe: 'Runtime probe', settings: 'Settings' }, source: 'Source: ', blocksSend: 'Blocks sending', blocksCapability: 'Blocks capability', - signalLabel: englishSignalLabel, - signalMessage: englishSignalMessage, - signalDetail: englishSignalDetail, + signalLabel: (signal) => (signal.id.endsWith(':runtime') ? `${signal.label} runtime` : signal.label), + signalMessage: (signal) => signalMessagesEn[signal.message], + signalDetail: (signal) => signalDetailEn(signal), } } satisfies UiCatalog; @@ -270,6 +270,13 @@ const connectionTestErrorMessages = { network: 'Network error', unknown: 'Connection test failed', }, + ko: { + auth: 'Authentication failed', + timeout: 'Request timed out', + provider_unavailable: 'Model service returned an error', + network: 'Network error', + unknown: 'Connection test failed', + }, } satisfies UiCatalog>; function signalDetailZh(detail: HealthSignalDetail | undefined): string | undefined { diff --git a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts index 5fdaafffbb..880ecda170 100644 --- a/apps/desktop/src/renderer/locales/settings-navigation-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-navigation-copy.ts @@ -128,7 +128,7 @@ const SETTINGS_NAVIGATION_COPY_BY_LOCALE = { 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, runtime environment, and privacy commitments.' }, + about: { label: 'About', description: 'Version, updates, and support.' }, }, } } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/locales/settings-shared-copy.ts b/apps/desktop/src/renderer/locales/settings-shared-copy.ts index 854338f7e2..c6359eed57 100644 --- a/apps/desktop/src/renderer/locales/settings-shared-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-shared-copy.ts @@ -226,8 +226,6 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { 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.', - buildInfo: 'Build info', - reference: 'Reference', }, } } satisfies UiCatalog; 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 7535db0511..a186505db2 100644 --- a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts @@ -170,6 +170,8 @@ const COPY = { .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) => @@ -190,6 +192,16 @@ const COPY = { "Enter an App ID and App Secret before testing the connection.", connectionFailed: "Check the credentials and network settings, then try again.", + errors: { + slack_tokens_missing: 'Enter a Slack Bot Token and App-Level Token before testing the connection.', + wecom_credentials_missing: 'Enter a WeCom Bot ID and Secret before testing the connection.', + dingtalk_credentials_missing: 'Enter a DingTalk Client ID (AppKey) and Client Secret before testing the connection.', + dingtalk_no_access_token: 'DingTalk returned no access_token. Check the credentials and network, then try again.', + qq_credentials_missing: 'Enter a QQ App ID and AppSecret before testing the connection.', + qq_no_access_token: 'QQ returned no access_token. Check the credentials and network, then try again.', + wechat_bridge_url_invalid: 'The local WeChat bridge only accepts the local wechat-bridge, not a remote URL.', + wechat_ilink_credentials_incomplete: 'Complete WeChat QR sign-in first to save the iLink bot token and base URL.', + }, }, } } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/locales/task-readiness-copy.ts b/apps/desktop/src/renderer/locales/task-readiness-copy.ts index 0c66a86515..bbd06b761b 100644 --- a/apps/desktop/src/renderer/locales/task-readiness-copy.ts +++ b/apps/desktop/src/renderer/locales/task-readiness-copy.ts @@ -69,6 +69,18 @@ const TASK_READINESS_COPY = { actionLabel: { workspace_picker: 'Choose workspace', retry: 'Check again' }, }, }, + ko: { + runtime: { + title: 'The Maka runtime is unavailable.', + description: 'The task was not submitted. Check the runtime again before retrying.', + actionLabel: 'Check again', + }, + workspace: { + title: 'This task workspace is unavailable.', + description: 'The folder may have moved, been deleted, or become inaccessible. Choose an available workspace.', + actionLabel: { workspace_picker: 'Choose workspace', retry: 'Check again' }, + }, + }, } satisfies UiCatalog; export function getTaskReadinessCopy(locale: UiLocale): TaskReadinessCopy { diff --git a/apps/desktop/src/renderer/locales/workhub-copy.ts b/apps/desktop/src/renderer/locales/workhub-copy.ts index 2c717ef125..06cc4b212d 100644 --- a/apps/desktop/src/renderer/locales/workhub-copy.ts +++ b/apps/desktop/src/renderer/locales/workhub-copy.ts @@ -79,6 +79,19 @@ const COPY = { { id: 'stopped', label: 'Stopped' }, ], }, + ko: { + work: 'Work', workNavigation: 'Work navigation', filterWork: 'Filter work', focused: 'Focused', + archived: 'Archived', + states: { active: 'Active', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Blocked', aborted: 'Aborted' }, + anchorCount: (shown, matching, total) => `${shown}/${matching} anchors · ${total} total`, + noFilteredWork: 'No work matches this filter', + filters: [ + { id: 'all', label: 'All' }, + { id: 'active', label: 'Active' }, + { id: 'attention', label: 'Needs you' }, + { id: 'stopped', label: 'Stopped' }, + ], + }, } satisfies UiCatalog; export function getWorkHubRailCopy(locale: UiLocale): WorkHubRailCopy { diff --git a/apps/desktop/src/renderer/mcp-catalog.ts b/apps/desktop/src/renderer/mcp-catalog.ts index 09b15f47ef..a463ab1909 100644 --- a/apps/desktop/src/renderer/mcp-catalog.ts +++ b/apps/desktop/src/renderer/mcp-catalog.ts @@ -221,6 +221,22 @@ const MCP_CATALOG_COPY = { playwright: { name: 'Browser automation', description: 'Let Maka read and operate real web pages through Playwright.', category: 'Design and development', setupLabel: undefined }, 'sequential-thinking': { name: 'Sequential thinking', description: 'Provide revisable, verifiable structured reasoning for complex problems.', category: 'Reasoning and planning', setupLabel: undefined }, }, + ko: { + dingtalk: { name: 'DingTalk', description: 'Manage contacts, calendars, tasks, and collaboration data.', category: 'Communication', setupLabel: 'Requires Client ID and Client Secret' }, + feishu: { name: 'Feishu', description: 'Access Feishu documents, calendars, messages, and OpenAPI.', category: 'Communication', setupLabel: 'Requires App ID and App Secret' }, + slack: { name: 'Slack', description: 'Send messages, manage channels, and collaborate in a Slack workspace.', category: 'Communication', setupLabel: 'Requires Bot Token and Team ID' }, + line: { name: 'LINE', description: 'Send and manage messages through the LINE Bot Messaging API.', category: 'Communication', setupLabel: 'Requires Channel Access Token' }, + notion: { name: 'Notion', description: 'Search, read, and update a Notion workspace.', category: 'Knowledge and documents', setupLabel: 'Requires sign-in authorization' }, + 'macos-apps': { name: 'macOS apps', description: 'Connect Calendar and Reminders through native system permissions.', category: 'System and productivity', setupLabel: undefined }, + 'google-calendar': { name: 'Google Calendar', description: 'Manage events, create meetings, and check availability.', category: 'System and productivity', setupLabel: 'Requires an OAuth credentials file' }, + figma: { name: 'Figma', description: 'Read design files, components, and developer handoff data.', category: 'Design and development', setupLabel: 'Requires a Personal Access Token' }, + vercel: { name: 'Vercel', description: 'Inspect projects, deployments, logs, and platform documentation.', category: 'Design and development', setupLabel: 'Requires sign-in authorization' }, + supabase: { name: 'Supabase', description: 'Manage databases, project configuration, migrations, and Edge Functions.', category: 'Design and development', setupLabel: 'Requires sign-in authorization' }, + filesystem: { name: 'Local files', description: 'Safely read, write, and manage files in selected directories.', category: 'Files and knowledge', setupLabel: 'Requires selecting allowed directories' }, + memory: { name: 'Persistent memory', description: 'Remember entities, relationships, and important facts in a structured knowledge graph.', category: 'Files and knowledge', setupLabel: undefined }, + playwright: { name: 'Browser automation', description: 'Let Maka read and operate real web pages through Playwright.', category: 'Design and development', setupLabel: undefined }, + 'sequential-thinking': { name: 'Sequential thinking', description: 'Provide revisable, verifiable structured reasoning for complex problems.', category: 'Reasoning and planning', setupLabel: undefined }, + }, } satisfies UiCatalog>; export function getMcpCatalog(locale: UiLocale): McpCatalogEntry[] { diff --git a/packages/core/src/redaction.ts b/packages/core/src/redaction.ts index fef441ccd2..8308ad1a86 100644 --- a/packages/core/src/redaction.ts +++ b/packages/core/src/redaction.ts @@ -279,11 +279,11 @@ export const GENERALIZED_ERROR_COPY = { network_error: 'Network error', }, ko: { - timeout: '요청 시간 초과', - rate_limited: '모델 속도 제한 초과', - auth_failed: '인증 실패', - provider_error: '모델 서비스 오류', - network_error: '네트워크 오류', + timeout: 'Request timed out', + rate_limited: 'Rate limit exceeded', + auth_failed: 'Authentication failed', + provider_error: 'Provider returned an error', + network_error: 'Network error', }, } satisfies UiCatalog>; diff --git a/packages/ui/src/__tests__/search-modal-source.test.ts b/packages/ui/src/__tests__/search-modal-source.test.ts index 64281c9e08..b23cb15181 100644 --- a/packages/ui/src/__tests__/search-modal-source.test.ts +++ b/packages/ui/src/__tests__/search-modal-source.test.ts @@ -153,6 +153,7 @@ describe('search error copy', () => { 'zh-CN': '搜索词无效,请缩短内容或移除凭据后重试。', 'zh-TW': '搜尋詞無效,請縮短內容或移除憑證後重試。', en: 'Invalid search query. Shorten it or remove credential material and try again.', + ko: 'Invalid search query. Shorten it or remove credential material and try again.', } satisfies UiCatalog; for (const locale of UI_LOCALES) { for (const query of ['a'.repeat(501), 'password=supersecret']) { diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts index 349c60a1ad..49395edd7e 100644 --- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts +++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts @@ -80,6 +80,7 @@ describe('tool activity presentation', () => { 'zh-CN': '需要“绕过”模式。此操作会直接控制本机应用,无法在沙箱模式下执行。', 'zh-TW': '需要“繞過”模式。此操作會直接控制本機應用,無法在沙箱模式下執行。', en: 'Bypass mode required. This action controls a local app directly and cannot run inside the sandbox.', + ko: 'Bypass mode required. This action controls a local app directly and cannot run inside the sandbox.', } satisfies UiCatalog; for (const locale of UI_LOCALES) { const row = renderToStaticMarkup(createElement(ToolTrow, { items: [item] }), locale); diff --git a/packages/ui/src/astryx-i18n.tsx b/packages/ui/src/astryx-i18n.tsx index 08f0e06fa2..ce74ccdc3a 100644 --- a/packages/ui/src/astryx-i18n.tsx +++ b/packages/ui/src/astryx-i18n.tsx @@ -99,6 +99,12 @@ const OVERRIDES_BY_LOCALE = { }, 'zh-CN': chineseOverrides('zh-CN', ASTRYX_COPY_ZH), 'zh-TW': chineseOverrides('zh-TW', ASTRYX_COPY_ZH_TW), + ko: { + en: { + '@astryx.chatComposerDrawer.collapse': 'Click to collapse {label}', + '@astryx.chatComposerDrawer.expand': 'Click to expand {label}', + }, + }, } satisfies UiCatalog; export function astryxMessageOverrides(locale: UiLocale): Overrides { diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 24ff7fd280..ff6b276d1e 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -1066,6 +1066,7 @@ const CONVERSATION_COPY = { 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' }, + forms: { requester: (name) => `Requested by ${name}`, requesterWithSource: (name, source) => `Requested by ${name} · ${source}`, required: 'Required', optional: 'Optional', include: (label) => `Provide ${label}`, enabled: (label) => `Enable ${label}`, enterValue: 'Enter a value', enterNumber: 'Enter a number', constraintSeparator: ' · ', lengthConstraint: (minimum, maximum) => minimum === undefined ? `At most ${maximum} characters` : maximum === undefined ? `At least ${minimum} characters` : `${minimum}–${maximum} characters`, numberConstraint: (minimum, maximum) => minimum === undefined ? `Maximum ${maximum}` : maximum === undefined ? `Minimum ${minimum}` : `Range ${minimum}–${maximum}`, itemConstraint: (minimum, maximum) => minimum === undefined ? `Select at most ${maximum}` : maximum === undefined ? `Select at least ${minimum}` : `Select ${minimum}–${maximum}`, formatConstraint: { email: 'Format: email', uri: 'Format: URI', date: 'Format: date (YYYY-MM-DD)', 'date-time': 'Format: date-time (RFC 3339)' }, invalid: 'Provide a value that meets the requirements.', cancel: 'Cancel', decline: 'Decline', accept: 'Submit', submitting: 'Submitting…' }, 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', @@ -1073,18 +1074,44 @@ const CONVERSATION_COPY = { 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', + you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', awaitingModelOutput: 'Waiting for model output…', providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, { day: 'd', hour: 'h', minute: 'm', second: 's' })} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `Waiting to retry (${attempt}/${maxAttempts})`, providerRetryReason: { stream_truncated: 'Response stream ended before completion', 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' }, failureDetailsUnavailable: 'No diagnostic details are available.', 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.', + contextCompacting: 'Compacting context…', + contextCompacted: 'Earlier context compacted.', + contextCompactionFailedOpen: 'Context compaction failed.', + contextProviderDropping: (used, prior) => + `The provider is dropping or rewriting context: content was appended, and it counted ${used.toLocaleString('en-US')} input tokens against ${prior.toLocaleString('en-US')} before, which is no growth. Declare a context window for this model in the connection settings so Maka compacts first.`, + contextWindowSuggestion: (tokens, declared) => + declared === undefined + ? `The provider rejected this request. No context window is declared for this model; the last accepted usage was about ${tokens} tokens — set the window to that value so Maka compacts first.` + : `The provider rejected this request at about ${tokens} tokens, below your declared window (${declared}). The declared value is likely larger than the provider's; consider lowering it to ${tokens}.`, + contextWindowOverrun: (used, declared) => + `This exchange used about ${used} tokens against your declared window (${declared}): the reply needed more room than was left. Maka compacts before the next request; raise the window if the replies should stay whole.`, + contextReportedWindowExceeded: (used, reported) => + `This exchange used about ${used} tokens, past the ${reported} this model reports, and the provider accepted it without complaint. Nothing is declared, so Maka does not compact on its own. Declare a context window in the connection settings to have it compact first.`, + contextOverflowAfterCompaction: + 'History was compacted and the provider still called this request too large. What remains also carries the system prompt, the tool schemas, the summary and the recent tail; shortening this message is the part you control.', + contextUsageLabel: 'Usage', + contextUsageShare: (used, window) => + `This request used ${used.toLocaleString('en-US')} / ${window.toLocaleString('en-US')} tokens (${Math.round((used / window) * 100)}%).`, + contextUsageNoWindow: (used) => + `This request used ${used.toLocaleString('en-US')} tokens; no context limit is available for this model.`, + contextUsageUnavailable: 'No usage data is available for this request.', + contextUsageOpen: 'Open usage trace', stepLimit: 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.', }, }, chat: { conversationAriaLabel: (name) => `Conversation: ${name}`, + transcriptGap: { + olderDescription: 'Earlier messages above are not loaded.', + olderAction: 'Load earlier messages', + newerDescription: 'Newer messages below are not loaded.', + newerAction: 'Load newer messages', + }, 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', @@ -1113,14 +1140,14 @@ const CONVERSATION_COPY = { 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', + titlebarIdentityAriaLabel: 'Current task', openProjectFolderAction: 'Open project folder', projectInfo: 'Project information', copyProjectPath: 'Copy path', + openParentSession: (name) => `Return to parent task “${name}”`, 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}`, selectRow: 'Select', selectionBarAriaLabel: 'Bulk actions for selected tasks', selectedCount: (selected, total) => `${selected} / ${total} selected`, selectAllAriaLabel: 'Select all or none', selectionArchive: 'Archive', selectionDelete: 'Delete', selectionClear: 'Done', + 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', projects: 'Projects', 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; diff --git a/packages/ui/src/runtime-resume-copy.ts b/packages/ui/src/runtime-resume-copy.ts index 7ffb412374..a5b294d7a9 100644 --- a/packages/ui/src/runtime-resume-copy.ts +++ b/packages/ui/src/runtime-resume-copy.ts @@ -187,6 +187,49 @@ const RESUME_PARK_COPY = { resume_feature_disabled: 'Resuming interrupted tasks is not enabled.', }, }, + ko: { + title: 'This round cannot be resumed yet', + fallbackDescription: 'This task does not currently meet the conditions to continue.', + missingCandidateTitle: 'Nothing to resume', + missingCandidateDescription: 'This task is already up to date.', + reasons: { + dangling_tool_state: + 'The previous tool run was interrupted; its records are preserved, so it cannot continue automatically yet.', + pending_permission: 'The previous run is still waiting for a permission approval.', + background_operation_pending: 'Background operations are still running, so this round cannot continue yet.', + workspace_identity_mismatch: 'The current workspace does not match the one from the interrupted run.', + workspace_identity_missing: 'The workspace from the interrupted run could not be identified.', + workspace_cwd_mismatch: 'The current working directory does not match the one from the interrupted run.', + workspace_ref_missing: 'The workspace from the interrupted run is no longer available.', + tool_catalog_mismatch: 'The available tools have changed, so it is not safe to continue.', + checkpoint_restore_failed: 'Restoring the workspace checkpoint failed.', + source_run_unreadable: "The previous run's record could not be read in full.", + runtime_ledger_unreadable: "The previous run's ledger could not be read in full.", + runtime_ledger_empty: 'The previous run has no records to replay.', + terminal_repair_failed: "Repairing the previous run's record failed.", + provider_resume_head_unsupported: 'The current model does not support this resume point.', + provider_resume_boundary_unsupported: 'The current model does not support this resume boundary.', + provider_replay_non_suffix_gap: 'The interruption point in the previous model output cannot be trimmed safely.', + provider_replay_unsupported: + "The previous run's history cannot be replayed safely under the current model protocol.", + runtime_lineage_cycle: 'The resume chain contains a cycle; resuming was stopped.', + runtime_lineage_depth_exceeded: 'The resume chain is too long; automatic resuming was stopped.', + runtime_lineage_missing: 'The resume chain is missing required history records.', + runtime_lineage_start_mismatch: "The resume chain's starting record is inconsistent; resuming was stopped.", + runtime_lineage_replay_mismatch: + "The resume chain's recorded model context does not match what was rebuilt here.", + runtime_lineage_claim_mismatch: + 'The resume chain lacks a matching resume-ownership record; resuming was stopped.', + source_prefix_digest_mismatch: "The previous run's immutable boundary has changed.", + continuation_already_exists: 'A continuation for this interrupted task already exists.', + continuation_claim_repair_required: + 'Resume ownership was preserved, but the continuation record needs repair first.', + continuation_started_indeterminate: + 'The continuation already started, but has not reached a provable terminal state.', + continuation_authority_unavailable: 'The current storage does not support safe resume ownership.', + resume_feature_disabled: 'Resuming interrupted tasks is not enabled.', + }, + }, } satisfies UiCatalog; export function resumeParkToastCopy(reasons: readonly string[], locale: UiLocale): ResumeParkToastCopy { diff --git a/packages/ui/src/session-hover-card-copy.ts b/packages/ui/src/session-hover-card-copy.ts index 5d1178fe26..26ddbd7b36 100644 --- a/packages/ui/src/session-hover-card-copy.ts +++ b/packages/ui/src/session-hover-card-copy.ts @@ -70,19 +70,20 @@ const COPY: Record = { 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: '디렉터리 사용 불가', + sessionDetailsLabel: (name) => `${name} task details`, + projectDetailsLabel: (name) => `${name} project details`, + groupDetailsLabel: (name) => `${name} group details`, + noMessages: 'No messages yet', + updated: 'Updated', + taskCount: (count) => `${count} ${count === 1 ? 'task' : 'tasks'}`, + runningTaskCount: (count) => `${count} running`, + locationCount: (count) => `${count} ${count === 1 ? 'location' : 'locations'}`, + projectAvailable: 'Directory available', + projectUnavailable: 'Directory unavailable', }, }; + export function getSessionHoverCardCopy(locale: UiLocale): SessionHoverCardCopy { return COPY[locale]; } diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index d389c19fe2..6c1c53f460 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -416,13 +416,6 @@ const SHARED_UI_COPY = { 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' }, diff --git a/packages/ui/src/shell-controls-copy.ts b/packages/ui/src/shell-controls-copy.ts index 2fc608a758..2cd36a13ce 100644 --- a/packages/ui/src/shell-controls-copy.ts +++ b/packages/ui/src/shell-controls-copy.ts @@ -160,19 +160,18 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { 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.', + errorByReason: { + incognito_active: 'Turn off privacy mode to search previous tasks by keyword.', + invalid_query: 'Invalid search query. Shorten it or remove credential material and try again.', + aborted: 'Search was canceled.', + disabled: 'Search is unavailable right now.', + provider_error: 'Search failed. Try again.', + }, 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', }, } diff --git a/packages/ui/src/skills-copy.ts b/packages/ui/src/skills-copy.ts index 929111ef2b..a1ee5c1ba4 100644 --- a/packages/ui/src/skills-copy.ts +++ b/packages/ui/src/skills-copy.ts @@ -207,7 +207,7 @@ const SKILLS_COPY = { 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.' }, + bundledDescription: { 'computer-use': 'Inspect and operate local desktop app interfaces.' }, 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' }, diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts index 26e0a40920..f26f04e98f 100644 --- a/packages/ui/src/tool-activity/copy.ts +++ b/packages/ui/src/tool-activity/copy.ts @@ -518,6 +518,7 @@ const TOOL_ACTIVITY_COPY = { requiresBypass: { title: 'Bypass mode required', description: 'This action controls a local app directly and cannot run inside the sandbox.', + errorMessage: 'Bypass mode required. This action controls a local app directly and cannot run inside the sandbox.', action: 'Switch and retry', pending: 'Switching…', }, @@ -577,6 +578,9 @@ const TOOL_ACTIVITY_COPY = { genericAction: 'Enable tool capabilities', genericTitle: 'Tool capabilities enabled', genericDescription: 'This tool group is ready to use.', + fallbackLabel: 'Tools', + namedAction: (label) => `Enable ${label}`, + namedTitle: (label) => `${label} enabled`, count: (n) => `${n} ${n === 1 ? 'capability' : 'capabilities'} available`, technicalDetails: 'Technical details', groupId: 'Group',