Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ async function seedE2eConnection(userDataDir: string): Promise<void> {
}
}

async function seedE2eLocale(userDataDir: string, locale: 'zh-CN' | 'zh-TW' | 'en'): Promise<void> {
async function seedE2eLocale(userDataDir: string, locale: 'zh-CN' | 'zh-TW' | 'en' | 'ko'): Promise<void> {
const workspaceRoot = path.join(userDataDir, 'workspaces', 'default');
await createSettingsStore(workspaceRoot).update({
personalization: { uiLocale: locale },
Expand Down Expand Up @@ -426,7 +426,7 @@ async function withE2eWindow(
seed: boolean;
readinessSelector: string;
e2eFixtureScenario?: string;
locale?: 'zh-CN' | 'zh-TW' | 'en';
locale?: 'zh-CN' | 'zh-TW' | 'en' | 'ko';
/** Opt this window back into animated scrolling; see `scroll-motion-policy`. */
scrollMotion?: 'auto' | 'smooth';
/** #1312: force app:info's platform so the window boots natively into that platform's `data-os` cascade. */
Expand Down Expand Up @@ -562,6 +562,7 @@ async function resetPromptRailWindow(worker: PromptRailWorker): Promise<void> {

type E2eTestFixtures = {
window: Page;
koreanWindow: Page;
agentGraphWindow: Page;
onboardingWindow: Page;
gitReviewWindow: { page: Page; projectRoot: string };
Expand Down Expand Up @@ -623,6 +624,9 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
window: async ({}, use) => {
await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'zh-CN' }, use);
},
koreanWindow: async ({}, use) => {
await withE2eWindow({ seed: true, readinessSelector: COMPOSER_INPUT, locale: 'ko' }, use);
},
agentGraphWindow: async ({}, use) => {
await withE2eWindow(
{
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ async function choiceContentGeometry(card: import('@playwright/test').Locator) {
});
}

test('Korean locale renders the localized settings navigation and page', async ({
koreanWindow: page,
}) => {
await page.getByRole('button', { name: 'Expand sidebar' }).click();
await page.getByRole('button', { name: 'Settings', exact: true }).click();

const settings = page.getByRole('main', { name: '설정 내용' });
await expect(settings).toBeVisible();
const navigation = page.getByRole('navigation', { name: '설정 섹션' });
await expect(navigation.getByRole('button', { name: '일반적인', exact: true })).toBeVisible();
await expect(navigation.getByRole('button', { name: '모델', exact: true })).toBeVisible();
await navigation.getByRole('button', { name: '하위 에이전트', exact: true }).click();
await expect(settings.getByRole('heading', { name: '하위 에이전트', exact: true })).toBeVisible();
});

test('Settings loading surface owns unmodified Escape', async ({ window: page }) => {
const latchInstalled = await page.evaluate(() => {
const e2eLatch = (window as unknown as SettingsChunkLatchWindow).makaE2eLatch;
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -3209,7 +3209,6 @@
"./settings-route-focus": 1,
"./settings-route-header": 1,
"@astryxdesign/core": 1,
"@maka/core/llm-connections": 1,
"@maka/core/provider-registry": 1,
"@maka/ui": 1,
"@maka/ui/icons": 1,
Expand Down
173 changes: 173 additions & 0 deletions apps/desktop/src/main/__tests__/korean-catalogs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { getArtifactCopy } from '../../renderer/locales/artifact-copy.js';
import { getBrowserCopy } from '../../renderer/locales/browser-copy.js';
import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js';
import { getExternalSessionImportCopy } from '../../renderer/locales/external-session-import-copy.js';
import { getMcpCopy } from '../../renderer/locales/mcp-copy.js';
import { getPlanModeCopy } from '../../renderer/locales/plan-mode-copy.js';
import { getBotSettingsCopy } from '../../renderer/locales/settings-bot-copy.js';
import { getDailyReviewSettingsCopy } from '../../renderer/locales/settings-daily-review-copy.js';
import { getDataSettingsCopy } from '../../renderer/locales/settings-data-copy.js';
import { getHealthCenterCopy } from '../../renderer/locales/settings-health-copy.js';
import type { HealthSignal } from '@maka/core/health';
import { getMemorySettingsCopy } from '../../renderer/locales/settings-memory-copy.js';
import { getSettingsNavigationCopy } from '../../renderer/locales/settings-navigation-copy.js';
import { getSettingsProjectsCopy } from '../../renderer/locales/settings-projects-copy.js';
import { getProviderSettingsCopy } from '../../renderer/features/connection-settings/index.js';
import { getSettingsSharedCopy } from '../../renderer/locales/settings-shared-copy.js';
import { getSubagentSettingsCopy } from '../../renderer/locales/settings-subagents-copy.js';
import { getSettingsTasksCopy } from '../../renderer/locales/settings-tasks-copy.js';
import { settingsTestResultMessage } from '../../renderer/locales/settings-test-result-copy.js';
import type { SettingsTestResult } from '@maka/core/settings';
import { getUsageSettingsCopy } from '../../renderer/locales/settings-usage-copy.js';
import { getWebSearchSettingsCopy } from '../../renderer/locales/settings-web-search-copy.js';
import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js';

function collectStrings(value: unknown): string[] {
if (typeof value === 'string') return [value];
if (typeof value === 'function' || value === null || typeof value !== 'object') return [];
return Object.values(value).flatMap(collectStrings);
}

test('all Desktop slice-2 catalogs expose visibly Korean copy', () => {
const values = [
getArtifactCopy('ko').pane.copy,
getBrowserCopy('ko').title,
getDesktopConversationCopy('ko').actions.conversationErrorTitle,
getExternalSessionImportCopy('ko').sourceLabel,
getMcpCopy('ko').page.setupTitle,
getPlanModeCopy('ko').proposal.execute,
getBotSettingsCopy('ko').overview.reload,
getDailyReviewSettingsCopy('ko').aria,
getDataSettingsCopy('ko').backupTitle,
getHealthCenterCopy('ko').title,
getMemorySettingsCopy('ko').text.title,
getSettingsNavigationCopy('ko').sections.general.label,
getSettingsProjectsCopy('ko').runtimeHost.title,
getProviderSettingsCopy('ko').detail.modelKey,
getSettingsSharedCopy('ko').modalLabel,
getSubagentSettingsCopy('ko').section.title,
getSettingsTasksCopy('ko').listAria,
getUsageSettingsCopy('ko').details,
getWebSearchSettingsCopy('ko').search,
];

for (const value of values) assert.match(value, /[가-힣]/u);
const proxyDisabled: SettingsTestResult = { ok: false, code: 'proxy_disabled', message: '' };
assert.match(settingsTestResultMessage(proxyDisabled, 'ko'), /[가-힣]/u);
});

test('Korean catalog additions do not leak Korean into English or leave key dynamic copy in English', () => {
const catalogs = [
getArtifactCopy,
getBrowserCopy,
getDesktopConversationCopy,
getExternalSessionImportCopy,
getMcpCopy,
getPlanModeCopy,
getBotSettingsCopy,
getDailyReviewSettingsCopy,
getDataSettingsCopy,
getHealthCenterCopy,
getMemorySettingsCopy,
getSettingsNavigationCopy,
getSettingsProjectsCopy,
getProviderSettingsCopy,
getSettingsSharedCopy,
getSubagentSettingsCopy,
getSettingsTasksCopy,
getUsageSettingsCopy,
getWebSearchSettingsCopy,
];
for (const getCopy of catalogs) {
assert.equal(collectStrings(getCopy('en')).some((value) => /[가-힣]/u.test(value)), false);
}

assert.match(getMemorySettingsCopy('ko').text.localFile, /[가-힣]/u);
const provider = getProviderSettingsCopy('ko');
for (const value of [
provider.detail.status,
provider.detail.filterModels,
provider.detail.declareCapabilities,
provider.shared.filterMatches(2),
provider.panel.groups.recommended,
provider.panel.connectionsHelp,
provider.panel.browseAll,
provider.add.stepsAria,
provider.add.onboardingSelectedCount(2, 3),
provider.add.onboardingDefaultModelHelp,
provider.oauthFlow.refreshFailed,
]) {
assert.match(value, /[가-힣]/u);
}
assert.match(getHealthCenterCopy('ko').signalMessage({
id: 'connection:test:runtime',
label: '테스트',
scope: 'llm_connection',
layer: 'runtime_probe',
status: 'ok',
source: 'runtime_probe',
checkedAt: 0,
message: '',
} satisfies HealthSignal), /[가-힣]/u);
const health = getHealthCenterCopy('ko');
assert.equal(health.signalLabel({
id: 'capability:activity_recorder',
label: 'Activity Recorder',
scope: 'capability',
layer: 'feature',
status: 'ok',
source: 'capability_snapshot',
checkedAt: 0,
message: '',
} satisfies HealthSignal), '활동 기록');
assert.equal(health.signalLabel({
id: 'capability:bot:telegram',
label: 'telegram Bot',
scope: 'bot',
layer: 'feature',
status: 'ok',
source: 'capability_snapshot',
checkedAt: 0,
message: '',
} satisfies HealthSignal), 'telegram 봇');
const inspector = getDesktopConversationCopy('ko').inspector;
assert.equal(inspector.callKind('daily_review'), '일일 검토');
assert.equal(inspector.permissionDecision('allow'), '허용됨');
assert.equal(inspector.retries(2), '2회 재시도');
assert.equal(getMcpCopy('ko').toast.importedDetail(2), '2 서버를 가져왔습니다.');
assert.equal(getMcpCopy('ko').row.tools(2), '2개 도구');
assert.equal(getArtifactCopy('ko').preview.externalLinks(2), '이 미리보기에서는 외부 링크가 비활성화되었습니다. · 외부 링크 2개');
assert.equal(getUsageSettingsCopy('ko').recordCount(2), '2개 기록');
assert.deepEqual(getSettingsPreferencesCopy('en').personalization.localeOptions.at(-1), ['ko', '한국어']);
assert.deepEqual(getSettingsPreferencesCopy('zh-CN').personalization.localeOptions.at(-1), ['ko', '한국어']);
});

test('Korean representative surfaces preserve interpolation', () => {
const plan = getPlanModeCopy('ko');
const conversation = getDesktopConversationCopy('ko');

assert.equal(plan.abandonConfirmation.description('Release plan'), '“Release plan”의 실행 기록은 유지되지만 다시 시작할 수 없습니다.');
assert.equal(plan.execution.stepCount(2, 3), '2/3단계');
assert.equal(conversation.actions.branchCreatedDescription('Release branch'), '새 작업: Release branch');
});
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ test('maps the non-default replacement choice to the replace decision', async ()
const prompts = createRuntimeHostUpgradePrompts(
async () => 'en',
async (options) => {
assert.deepEqual(options.buttons, ['Stop Host and Continue', 'Wait', 'Cancel Startup']);
assert.equal(options.defaultId, 2);
assert.equal(options.cancelId, 2);
assert.deepEqual(options.buttons, ['Stop Host and Continue', 'Cancel Startup']);
assert.equal(options.defaultId, 1);
assert.equal(options.cancelId, 1);
assert.match(options.detail ?? '', /Maka will stop this Host/);
return { response: 0, checkboxChecked: false };
},
Expand Down
14 changes: 10 additions & 4 deletions apps/desktop/src/main/__tests__/settings-preferences-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import { getBotSettingsCopy } from '../../renderer/locales/settings-bot-copy.js'
test('language selector offers every preference with locale-appropriate labels', () => {
assert.deepEqual(getSettingsPreferencesCopy('zh-CN').personalization.localeOptions, [
['auto', '跟随系统'],
['zh-CN', '简体中文'],
['zh-TW', '繁體中文'],
['zh-CN', '中文'],
['en', 'English'],
['ko', '한국어'],
]);
assert.deepEqual(getSettingsPreferencesCopy('zh-TW').personalization.localeOptions, [
['auto', '自動(跟隨系統)'],
Expand All @@ -39,9 +39,15 @@ test('language selector offers every preference with locale-appropriate labels',
]);
assert.deepEqual(getSettingsPreferencesCopy('en').personalization.localeOptions, [
['auto', 'Follow system'],
['zh-CN', 'Simplified Chinese'],
['zh-TW', 'Traditional Chinese'],
['zh-CN', '中文'],
['en', 'English'],
['ko', '한국어'],
]);
assert.deepEqual(getSettingsPreferencesCopy('ko').personalization.localeOptions, [
['auto', '시스템 설정 따름'],
['zh-CN', '中文'],
['en', 'English'],
['ko', '한국어'],
]);
});

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/computer-use/status-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ const COPY: UiCatalog<StatusItemCopy> = {
stopUnnamed: 'Stop Computer Use',
empty: 'No Active Sessions',
},
ko: {
stopUsing: (appName) => `${appName} 사용 중지`,
stopUnnamed: 'Computer Use 중지',
empty: '활성 세션 없음',
},
};

function defaultResolveLocale(): UiLocale {
Expand Down
46 changes: 46 additions & 0 deletions apps/desktop/src/main/native-diagnostic-dialog-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,52 @@ const COPY = {
exit: '退出',
},
},
ko: {
dialog: {
copy: '진단 정보 복사',
copyAgain: '다시 복사',
copied: '진단 정보가 복사되었습니다. 문제 보고서에 붙여넣을 수 있습니다.',
copyFailed: '진단 정보를 복사할 수 없습니다.',
},
fatalStartup: {
title: 'Maka를 시작하지 못했습니다',
message: 'Maka가 시작을 완료하지 못했습니다.',
detail: '시작 중 예기치 않은 오류가 발생했습니다. 진단 정보를 복사하면 자세한 내용을 확인할 수 있습니다.',
exit: '종료',
},
rendererGone: {
title: 'Maka 복구 필요',
message: 'Maka 인터페이스가 예기치 않게 중지되었습니다.',
detail: '인터페이스만 복구하며 Maka는 다시 시작하지 않습니다. Runtime Host와 실행 중인 작업은 유지됩니다.',
recover: '인터페이스 복구',
exit: '종료',
},
runtimeHostRecovery: {
title: 'Maka Runtime Host 수정 필요',
message: '이 작업 공간을 관리하는 Runtime Host를 시작할 수 없습니다.',
detail: 'Maka는 이 Desktop에서 선택한 Runtime Host를 수정할 수 있습니다. 작업 공간, Host ID, 자격 증명 및 설정은 유지됩니다.',
activeTasks: 'Host에서 실행 중인 작업이 있을 수 있습니다. 계속하면 먼저 중단한 후 Host를 다시 시작합니다.',
repairFailed: '이전 수정이 완료되지 않았습니다. 진단 정보를 복사하면 자세한 내용을 확인할 수 있습니다.',
repair: 'Runtime Host 수정',
repairAndRestart: '수정 후 Host 다시 시작',
exit: '종료',
},
defaultRuntimeHostRecovery: {
title: '기본 Runtime Host에 연결할 수 없습니다',
connectFailed: (profileName) => `${profileName}에 연결할 수 없습니다`,
detail: '다시 시도하거나 Local을 기본 Host로 사용하거나 현재 선택을 유지하고 나중에 설정에서 처리할 수 있습니다.',
retry: '다시 시도',
useLocal: 'Local 사용',
keepOffline: '오프라인 유지',
},
storageRootRepair: {
title: 'Maka 작업 공간 수정 필요',
message: 'Maka가 이 작업 공간을 확인할 수 없습니다.',
detail: (workspaceRoot) => `시스템의 디스크 ID가 변경되었을 수 있습니다. 복사된 작업 공간이 아닌 이 컴퓨터의 원래 Maka 작업 공간인 경우에만 수정하세요.\n\n${workspaceRoot}`,
repair: '작업 공간 수정',
exit: '종료',
},
},
} satisfies UiCatalog<NativeDiagnosticDialogCopy>;

export function getNativeDiagnosticDialogCopy(locale: UiLocale): NativeDiagnosticDialogCopy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/main/runtime-host-boot-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ 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<StartupRecoveryCopy>;

export function getStartupRecoveryCopy(locale: UiLocale): StartupRecoveryCopy {
Expand Down
Loading