diff --git a/.changeset/ko-locale-core.md b/.changeset/ko-locale-core.md new file mode 100644 index 00000000..8e8bed95 --- /dev/null +++ b/.changeset/ko-locale-core.md @@ -0,0 +1,6 @@ +--- +"@open-codesign/desktop": patch +"@open-codesign/i18n": patch +--- + +Feat: add Korean locale core support across the shared i18n runtime and desktop language selection UI. \ No newline at end of file diff --git a/apps/desktop/src/renderer/src/components/LanguageToggle.tsx b/apps/desktop/src/renderer/src/components/LanguageToggle.tsx index 9e04db6a..563a37fa 100644 --- a/apps/desktop/src/renderer/src/components/LanguageToggle.tsx +++ b/apps/desktop/src/renderer/src/components/LanguageToggle.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; const noDragStyle = { WebkitAppRegion: 'no-drag' } as CSSProperties; -const LOCALE_CYCLE: Locale[] = ['en', 'zh-CN', 'pt-BR']; +const LOCALE_CYCLE: Locale[] = ['en', 'zh-CN', 'pt-BR', 'ko']; function nextLocale(locale: Locale): Locale { const i = LOCALE_CYCLE.indexOf(locale); @@ -16,6 +16,7 @@ function nextLocale(locale: Locale): Locale { function localeLabel(locale: Locale): string { if (locale === 'zh-CN') return 'ZH'; if (locale === 'pt-BR') return 'PT'; + if (locale === 'ko') return 'KO'; return 'EN'; } diff --git a/apps/desktop/src/renderer/src/components/Settings.tsx b/apps/desktop/src/renderer/src/components/Settings.tsx index 3524b7ad..e922f05a 100644 --- a/apps/desktop/src/renderer/src/components/Settings.tsx +++ b/apps/desktop/src/renderer/src/components/Settings.tsx @@ -2155,6 +2155,7 @@ function AppearanceTab() { { value: 'en', label: t('settings.appearance.langEn') }, { value: 'zh-CN', label: t('settings.appearance.langZhCN') }, { value: 'pt-BR', label: t('settings.appearance.langPtBR') }, + { value: 'ko', label: t('settings.appearance.langKo') }, ]} /> diff --git a/packages/i18n/package.json b/packages/i18n/package.json index ca7cf030..768930af 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -8,7 +8,9 @@ "exports": { ".": "./src/index.ts", "./locales/en": "./src/locales/en.json", - "./locales/zh-CN": "./src/locales/zh-CN.json" + "./locales/zh-CN": "./src/locales/zh-CN.json", + "./locales/pt-BR": "./src/locales/pt-BR.json", + "./locales/ko": "./src/locales/ko.json" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/i18n/src/i18n.test.ts b/packages/i18n/src/i18n.test.ts index 5e510ea7..ac8e9392 100644 --- a/packages/i18n/src/i18n.test.ts +++ b/packages/i18n/src/i18n.test.ts @@ -12,6 +12,7 @@ describe('normalizeLocale', () => { it('returns the value unchanged when it is supported', () => { expect(normalizeLocale('en')).toBe('en'); expect(normalizeLocale('zh-CN')).toBe('zh-CN'); + expect(normalizeLocale('ko')).toBe('ko'); }); it('coalesces common Chinese variants to zh-CN', () => { @@ -26,6 +27,11 @@ describe('normalizeLocale', () => { expect(normalizeLocale('en-GB')).toBe('en'); }); + it('maps Korean variants to ko', () => { + expect(normalizeLocale('ko-KR')).toBe('ko'); + expect(normalizeLocale('ko_kr')).toBe('ko'); + }); + it('falls back to en for unsupported locales and warns', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); expect(normalizeLocale('fr-FR')).toBe('en'); @@ -62,6 +68,10 @@ describe('initI18n + setLocale (live switching)', () => { expect(i18n.t('chat.placeholder')).toBe('想设计什么?'); expect(i18n.t('common.preAlpha')).toBe('预览版'); + await setLocale('ko'); + expect(i18n.t('chat.placeholder')).toBe('무엇을 디자인할지 설명하세요…'); + expect(i18n.t('common.preAlpha')).toBe('프리 알파'); + await setLocale('en'); expect(i18n.t('common.send')).toBe('Send'); }); @@ -87,6 +97,10 @@ describe('initI18n + setLocale (live switching)', () => { expect(getCurrentLocale()).toBe('zh-CN'); expect(i18n.t('common.send')).toBe('发送'); + await setLocale('ko'); + expect(getCurrentLocale()).toBe('ko'); + expect(i18n.t('common.send')).toBe('보내기'); + await setLocale('en'); expect(getCurrentLocale()).toBe('en'); expect(i18n.t('common.send')).toBe('Send'); @@ -117,6 +131,29 @@ describe('onboarding i18n keys (Welcome / PasteKey / ChooseModel)', () => { expect(i18n.t('onboarding.choose.back')).toBe('Back'); }); + it('switches all onboarding strings to Korean when locale is ko', async () => { + const { i18n } = await import('./index'); + await initI18n('en'); + await setLocale('ko'); + + expect(i18n.t('onboarding.welcome.title')).toBe('어떤 모델로든 디자인하세요.'); + expect(i18n.t('onboarding.welcome.tryFree')).toBe('무료로 시작하기'); + expect(i18n.t('onboarding.welcome.useKey')).toBe('내 API 키 사용'); + expect(i18n.t('onboarding.welcome.whereToGetKey')).toBe('키를 받는 방법'); + + expect(i18n.t('onboarding.paste.title')).toBe('API 키 붙여넣기'); + expect(i18n.t('onboarding.paste.back')).toBe('뒤로'); + expect(i18n.t('onboarding.paste.continue')).toBe('계속'); + expect(i18n.t('onboarding.paste.connectionTest.button')).toBe('테스트'); + expect(i18n.t('onboarding.paste.connectionTest.ok')).toBe('연결됨'); + + expect(i18n.t('onboarding.choose.title')).toBe('기본 모델 선택'); + expect(i18n.t('onboarding.choose.finish')).toBe('완료'); + expect(i18n.t('onboarding.choose.back')).toBe('뒤로'); + + await setLocale('en'); + }); + it('switches all onboarding strings to Chinese when locale is zh-CN', async () => { const { i18n } = await import('./index'); await initI18n('en'); diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts index b975fbca..3359fa26 100644 --- a/packages/i18n/src/index.ts +++ b/packages/i18n/src/index.ts @@ -16,10 +16,11 @@ import i18next from 'i18next'; import { useCallback } from 'react'; import { initReactI18next, useTranslation } from 'react-i18next'; import en from './locales/en.json'; +import ko from './locales/ko.json'; import ptBR from './locales/pt-BR.json'; import zhCN from './locales/zh-CN.json'; -export const availableLocales = ['en', 'zh-CN', 'pt-BR'] as const; +export const availableLocales = ['en', 'zh-CN', 'pt-BR', 'ko'] as const; export type Locale = (typeof availableLocales)[number]; const DEFAULT_LOCALE: Locale = 'en'; @@ -28,6 +29,7 @@ const resources = { en: { translation: en }, 'zh-CN': { translation: zhCN }, 'pt-BR': { translation: ptBR }, + ko: { translation: ko }, } as const; export function isSupportedLocale(value: string | undefined | null): value is Locale { @@ -45,6 +47,9 @@ export function normalizeLocale(value: string | undefined | null): Locale { if (lower === 'pt-br' || lower === 'pt_br' || lower === 'pt' || lower.startsWith('pt-')) { return 'pt-BR'; } + if (lower === 'ko' || lower === 'ko-kr' || lower === 'ko_kr' || lower.startsWith('ko-')) { + return 'ko'; + } if (lower.startsWith('en')) return 'en'; console.warn( `[i18n] unsupported locale "${value}", falling back to "${DEFAULT_LOCALE}". ` + diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 72493250..fa974319 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -397,7 +397,8 @@ "languageLoadFailed": "Failed to load language", "langEn": "English", "langZhCN": "中文 (简体)", - "langPtBR": "Português (BR)" + "langPtBR": "Português (BR)", + "langKo": "한국어" }, "language": { "label": "Language", diff --git a/packages/i18n/src/locales/ko.json b/packages/i18n/src/locales/ko.json new file mode 100644 index 00000000..6033d2ac --- /dev/null +++ b/packages/i18n/src/locales/ko.json @@ -0,0 +1,1152 @@ +{ + "common": { + "appName": "Open CoDesign", + "send": "보내기", + "cancel": "취소", + "retry": "다시 시도", + "save": "저장", + "close": "닫기", + "dismissNotification": "알림 닫기", + "settings": "설정", + "advanced": "고급", + "about": "정보", + "learnMore": "자세히 보기", + "copy": "복사", + "copied": "복사됨", + "comingSoon": "곧 제공 예정", + "loading": "불러오는 중…", + "preAlpha": "프리 알파", + "tagline": "BYOK · 로컬 우선 · 멀티 모델", + "done": "완료.", + "applied": "적용됨.", + "working": "작업 중" + }, + "toast": { + "error": { + "report": "Report" + } + }, + "loading": { + "stage": { + "sending": "Connecting…", + "thinking": "Thinking…", + "streaming": "Generating…", + "parsing": "Finalizing…", + "rendering": "Rendering preview…", + "done": "Done" + }, + "tokens": "{{count}} tokens" + }, + "canvas": { + "filesTab": "Files", + "filesTabEmpty": "No files yet", + "openInTab": "Open in tab", + "previewHint": "Thumbnail preview · double-click the file or use Open in tab for full view", + "tabsAriaLabel": "Open files", + "closeTab": "Close {{name}}", + "files": { + "sectionTitle": "Design files", + "sectionSubtitle": "{{count}} files", + "empty": "No files yet. Send a prompt to have the agent generate a design — files will show up here." + }, + "newDesignDialog": { + "title": "New design", + "subtitle": "Optionally choose a local folder to sync your design files.", + "noWorkspace": "No folder selected", + "skip": "Skip", + "confirm": "Create" + }, + "workspace": { + "sectionTitle": "Workspace", + "label": "Folder", + "default": "Default internal storage", + "choose": "Choose folder", + "change": "Change", + "open": "Open folder", + "clear": "Clear workspace", + "unavailable": "Folder not found on disk", + "busyGenerating": "Cannot change workspace while generating", + "updated": "Workspace updated", + "updateFailed": "Failed to update workspace", + "rebindTitle": "Change workspace folder?", + "rebindDescription": "This design already has a workspace folder. Choose how to proceed:", + "rebindCancel": "Cancel", + "rebindSwitchOnly": "Switch without copying", + "rebindSwitchAndCopy": "Switch and copy files" + }, + "rail": { + "title": "Files", + "collapse": "Collapse files", + "expand": "Expand files", + "empty": "No files yet" + } + }, + "preview": { + "empty": { + "title": "Design with AI", + "body": "Pick a starter on the left, or describe what you want to design. The result renders here in a sandboxed preview.", + "starterChip": "Try a starter prompt:" + }, + "loading": { + "title": "Generating your design…" + }, + "error": { + "title": "Generation failed", + "body": "Something went wrong while generating your design.", + "copyError": "Copy error details", + "brokenJsx": "This design has a syntax error, likely an incomplete early save. Regenerate or edit to fix.", + "undefinedRef": "The design references an undefined variable or component. Likely a mid-run abort — try regenerating." + }, + "ready": "Preview", + "noDesign": "No design yet", + "clickToComment": "Click any element in the preview to leave an inline comment.", + "commentMode": "Comment mode", + "commentModeHint": "Click any element to comment", + "runtimeError": "Preview runtime error", + "dismissErrors": "Dismiss preview errors", + "zoom": "Zoom" + }, + "tweaks": { + "title": "Tweaks", + "openLabel": "Open tweaks panel", + "close": "Close", + "reset": "Reset to defaults", + "pickColor": "Pick color", + "swatchAria": "{{key}} color", + "emptyTitle": "No tweakable tokens", + "emptyHint": "The agent hasn't declared an EDITMODE block yet. Future generations will expose design tokens here." + }, + "chat": { + "placeholder": "무엇을 디자인할지 설명하세요…", + "sendShortcut": "보내기 (Enter)", + "emptyHint": "스타터 프롬프트나 직접 작성한 아이디어로 시작하세요.", + "sendAction": "보내기", + "sendAnywhere": "어디서나 보내기", + "send": "프롬프트 보내기", + "stop": "생성 중지", + "placeholderRich": "디자인을 설명하세요… 예: '핀테크 스타트업을 위한 피치덱'" + }, + "sidebar": { + "localContext": "Local context", + "attachLocalFiles": "Attach local files", + "linkDesignSystemRepo": "Link design system repo", + "refreshDesignSystemRepo": "Refresh design system repo", + "referenceUrl": "Reference URL", + "attachedFiles": "Attached files", + "removeFile": "Remove {{name}}", + "activeDesignSystem": "Active design system", + "clear": "Clear", + "designSystemHint": "Link a repo to extract colors, typography, spacing, and other styling cues for future generations.", + "startHint": "Start with a brief, then add files, a URL, or a local repo to ground the result.", + "empty": { + "title": "What should we design?", + "subtitle": "Describe your idea below — or start from one of these.", + "eyebrow": "Try" + }, + "ariaLabel": "Chat pane", + "noDesign": "No design", + "newChat": "New chat", + "collapse": "Collapse sidebar", + "expand": "Expand sidebar", + "chat": { + "youLabel": "You", + "claudeLabel": "Claude", + "noModel": "No model selected", + "addMenu": { + "trigger": "Add context" + }, + "thinking": "Thinking", + "streamingLabel": "Assistant is typing", + "tokensLine": "~{{count}} tokens", + "artifactDelivered": "delivered", + "artifactDefaultLabel": "design.html", + "tool": { + "done": "Done" + }, + "working": { + "title": "Working" + } + }, + "comments": { + "title": "Comment mode", + "body": "Inline comments on the canvas arrive in v0.2.1. You'll be able to click any element in the preview to leave a note or an edit instruction." + } + }, + "settings": { + "title": "설정", + "tabs": { + "models": "모델", + "images": "이미지 API", + "appearance": "모양", + "storage": "저장소", + "diagnostics": "진단", + "advanced": "고급" + }, + "imageGen": { + "tabTitle": "이미지 생성", + "tabHint": "디자인 에이전트가 hero, poster, background bitmap 생성을 위해 이미지 모델(예: gpt-image-2)을 호출하게 합니다. 인증 정보는 로컬에만 남습니다.", + "title": "이미지 생성 assist", + "hint": "켜면 agent가 실행 중 generate_image_asset을 호출할 수 있습니다.", + "enabled": "이미지 생성 assist 켜기", + "provider": "Provider", + "credentials": "인증 정보", + "inherit": "모델 provider에서 상속", + "customKey": "별도 키 사용", + "keyPlaceholder": "{{mask}} 유지하려면 비워 두세요", + "newKeyPlaceholder": "API 키 붙여넣기", + "model": "이미지 모델", + "baseUrl": "Base URL", + "quality": "품질", + "size": "크기", + "status": { + "ready": "준비됨", + "needsKey": "API 키 필요", + "disabled": "꺼짐" + }, + "toast": { + "loadFailed": "이미지 생성 설정을 불러오지 못했습니다", + "saved": "이미지 생성 설정 저장됨", + "saveFailed": "이미지 생성 설정 저장 실패" + } + }, + "shell": { + "back": "Workspace", + "backAria": "Workspace로 돌아가기" + }, + "common": { + "loading": "불러오는 중…", + "cancel": "취소", + "copy": "복사", + "copied": "복사됨!", + "open": "열기", + "unknownError": "알 수 없는 오류" + }, + "providers": { + "sectionTitle": "API Providers", + "chatgptLogin": { + "title": "Sign in with ChatGPT subscription", + "description": "Use your ChatGPT Plus / Pro / Team plan quota to call Codex models (gpt-5.3-codex and friends) — no API key needed.", + "signIn": "Sign in with ChatGPT", + "inProgress": "Browser opened. Complete the authorization and we'll return automatically…", + "loggedInBadge": "Signed in with ChatGPT", + "logout": "Sign out", + "confirmLogout": "Sign out of ChatGPT subscription?", + "loginFailedTitle": "ChatGPT sign-in failed", + "logoutFailedTitle": "ChatGPT sign-out failed", + "statusFailedTitle": "ChatGPT status check failed", + "unknownError": "Unknown error", + "unsupportedCountryRegion": "ChatGPT subscription login is not available for this account, country, region, or territory." + }, + "addProvider": "Provider 추가", + "addCustom": "Custom 추가", + "empty": "아직 설정된 provider가 없습니다. 생성을 시작하려면 하나를 추가하세요.", + "active": "활성", + "activeNotInList": "(활성, provider 목록에 없음)", + "decryptionFailed": "복호화 실패", + "setActive": "활성으로 설정", + "reEnterKey": "키 다시 입력", + "missingKey": "키 없음", + "addKey": "키 추가", + "confirm": "확인", + "delete": "삭제", + "edit": "편집", + "testConnection": "연결 테스트", + "moreActions": "더 보기", + "editModel": "모델 변경", + "deleteAria": "{{label}} provider 삭제", + "primary": "Primary", + "fast": "Fast", + "custom": { + "title": "Custom endpoint 추가", + "editTitle": "Provider 편집", + "wire": "Wire protocol", + "wires": { + "openai-chat": "OpenAI Chat", + "openai-responses": "OpenAI Responses", + "anthropic": "Anthropic Messages" + }, + "name": "Label", + "baseUrl": "Base URL", + "apiKey": "API Key", + "apiKeyEditPlaceholder": "{{mask}} 유지하려면 비워 두세요", + "defaultModel": "기본 모델", + "compatibilityHintTitle": "호환성 안내", + "compatibilityHintBody": "일부 coding plan, relay service, OpenAI-compatible gateway는 Claude Code, openclaw, Hermes 같은 특정 client만 허용할 수 있습니다. API 형태가 호환되어도 Open CoDesign은 앱 allowlist에 의해 차단될 수 있습니다.", + "switchToManual": "직접 입력", + "switchToDropdown": "목록에서 선택", + "discoveringModels": "모델 검색 중...", + "discoveredModels": "{{count}}개 모델 발견", + "discoveryFailed": "모델 자동 검색 실패", + "test": "연결 테스트", + "testOk": "OK — {{count}}개 모델 사용 가능", + "save": "저장하고 계속", + "saveEdit": "변경 저장" + }, + "import": { + "action": "가져오기", + "dismiss": "닫기", + "codexFound": "Codex config 감지됨 — {{count}}개 provider를 가져올까요?", + "claudeCodeOAuthTitle": "Claude Code subscription 감지됨", + "claudeCodeOAuthBody": "Pro/Max subscription은 Claude Code client 자체를 통해서만 사용할 수 있습니다. Third-party app은 subscription quota를 재사용할 수 없습니다. 여기서 사용하려면 Anthropic Console에서 API 키를 생성하세요(같은 계정, token 단위 과금).", + "claudeCodeOAuthCtaConsole": "Anthropic Console에서 API 키 받기 ↗", + "claudeCodeHasKeyBody": "{{source}}에서 {{baseUrl}} gateway용 키를 찾았습니다 — 가져오기를 눌러 사용하세요.", + "claudeCodeHasKeySourceSettings": "~/.claude/settings.json", + "claudeCodeHasKeySourceEnv": "shell env", + "claudeCodeLocalProxyBody": "{{baseUrl}}의 local proxy처럼 보입니다. Claude Code Proxy 같은 도구를 통해 OAuth subscription을 재사용하는 일반적인 설정입니다.", + "claudeCodeLocalProxyAction": "proxy를 쓰기 위해 키 붙여넣기", + "claudeCodeRemoteGatewayBody": "{{baseUrl}} gateway에 Claude Code config의 API 키가 없습니다. gateway를 가져온 뒤 설정에서 키를 붙여넣으세요.", + "claudeCodeRemoteGatewayAction": "gateway를 쓰기 위해 키 붙여넣기", + "oauthErrorToast": "Claude Code subscription은 third-party app과 공유할 수 없습니다. Anthropic Console에서 API 키를 생성하세요.", + "oauthErrorToastCta": "API 키 받기 ↗", + "codexDone": "Codex provider 가져옴", + "geminiFound": "Gemini CLI 키 감지됨 — 가져올까요?", + "geminiNoKey": "Gemini CLI config를 찾았지만 ~/.gemini/.env 또는 ~/.env에 API 키가 없습니다. Gemini를 쓰려면 키를 직접 붙여넣으세요.", + "geminiBlocked": "Google Vertex AI project는 아직 지원하지 않습니다 — Gemini를 쓰려면 Gemini Developer API 키(AIzaSy…로 시작)를 붙여넣으세요.", + "geminiDone": "Gemini provider 가져옴", + "opencodeBlocked": "OpenCode config를 찾았지만 가져올 수 있는 항목이 없습니다. 위 warning을 확인하고 auth.json을 수정한 뒤 다시 시도하세요.", + "opencodeFound": "OpenCode 감지됨 — {{count}}개 provider({{providers}})를 가져올까요?", + "opencodeDone": "OpenCode provider 가져옴", + "claudeCodeImportedActivated": "Claude Code를 가져오고 활성 모델 provider로 설정했습니다", + "claudeCodeOpenSettings": "설정 열기", + "claudeCodeIHaveKey": "API 키가 있습니다 — 붙여넣기", + "claudeCodeShellEnvHint": "또는 shell에서 ANTHROPIC_API_KEY를 export하고 Terminal에서 다시 실행하세요 — 자동으로 감지합니다.", + "claudeCodeParseErrorTitle": "Claude Code config 형식이 잘못되었습니다", + "claudeCodeParseErrorBody": "~/.claude/settings.json을 parse할 수 없습니다: {{reason}}.", + "claudeCodeParseErrorReasonNotObject": "최상위 값이 JSON object가 아닙니다", + "claudeCodeWarningsMore": "+{{count}}개 더", + "claudeCodeParseErrorCopyPath": "파일 경로 복사", + "claudeCodeParseErrorPathCopied": "경로가 clipboard에 복사되었습니다", + "claudeCodeAnthropicPresetName": "Anthropic", + "claudeCodeLocalProxyPresetName": "Claude Code Proxy (local)", + "claudeCodeRemoteGatewayPresetName": "Claude Code Gateway", + "failed": "가져오기 실패", + "codexMenu": "Codex에서 가져오기", + "codexMenuDesc": "~/.codex/config.toml 읽기", + "claudeCodeMenu": "Claude Code에서 가져오기", + "claudeCodeMenuDesc": "로그인된 Claude Code session 읽기", + "ollamaMenu": "Ollama (local)", + "ollamaMenuDesc": "localhost:11434 local provider 추가", + "ollamaDone": "Ollama provider 추가됨", + "customMenu": "Custom provider", + "customMenuDesc": "API 키와 URL을 직접 입력", + "alreadyImported": "이미 가져옴" + }, + "modal": { + "title": "Add provider", + "provider": "Provider", + "apiKey": "API Key", + "getKey": "Get key ↗", + "apiKeyPlaceholder": "sk-...", + "validate": "Validate", + "validating": "Validating…", + "valid": "Valid", + "baseUrl": "Base URL", + "baseUrlOptional": "(optional)", + "baseUrlPlaceholder": "https://your-proxy.example.com", + "primaryModel": "Primary model", + "fastModel": "Fast model", + "save": "Save provider" + }, + "toast": { + "loadFailed": "Failed to load providers", + "removed": "Provider removed", + "deleteFailed": "Delete failed", + "activateFailed": "Cannot activate provider", + "missingModel": "Provider has no default model — add one first.", + "switchedTo": "Switched to {{label}}", + "switchFailed": "Switch failed", + "saved": "Provider saved", + "saveFailed": "Failed to save provider", + "modelSaveFailed": "Failed to save model selection", + "reasoningSaved": "Reasoning depth saved", + "reasoningSaveFailed": "Failed to save reasoning depth", + "connectionOk": "Connection OK", + "connectionFailed": "Connection failed" + }, + "cliProxyApi": { + "presetName": "CLIProxyAPI", + "presetDescription": "Claude/Codex/Gemini OAuth subscription을 감싸는 local proxy", + "apiKeyOptional": "CPA config.yaml에 `api-keys`를 설정한 경우에만 API 키가 필요합니다", + "thinkingHint": "팁: 모델 이름에 `(high)` / `(xhigh)` / `(8192)`를 붙여 thinking budget을 제어하세요" + }, + "cpaDetection": { + "title": "이 컴퓨터에서 CLIProxyAPI가 감지되었습니다", + "body": "가져오면 OAuth 인증된 Claude / Codex / Gemini 계정을 provider로 사용할 수 있습니다.", + "importAction": "가져오기", + "dismissAction": "닫기" + }, + "reasoning": { + "label": "Reasoning depth", + "default": "기본값 (자동)", + "minimal": "최소", + "low": "낮음", + "medium": "보통", + "high": "높음", + "xhigh": "매우 높음" + } + }, + "appearance": { + "themeTitle": "테마", + "themeHint": "선택한 값은 재시작 후에도 유지됩니다.", + "lightLabel": "라이트", + "lightDesc": "따뜻한 베이지, 부드러운 그림자", + "darkLabel": "다크", + "darkDesc": "깊은 뉴트럴, 낮은 눈부심", + "languageLabel": "언어", + "languageHint": "언어 변경은 즉시 적용됩니다.", + "languageLoadFailed": "언어를 불러오지 못했습니다", + "langEn": "English", + "langZhCN": "中文 (简体)", + "langPtBR": "Português (BR)", + "langKo": "한국어" + }, + "language": { + "label": "언어", + "system": "시스템 기본값" + }, + "theme": { + "label": "테마", + "light": "라이트", + "dark": "다크", + "system": "시스템" + }, + "storage": { + "pathsTitle": "경로", + "config": "설정", + "logs": "로그", + "data": "데이터 디렉터리", + "change": "변경", + "restartHint": "open-codesign이 config, logs, local design data를 저장할 위치를 선택하세요. 변경 사항은 영구 저장되며 앱 재시작 후 적용됩니다.", + "locationSavedToast": "저장 위치가 저장되었습니다. 적용하려면 앱을 재시작하세요.", + "locationSaveFailed": "저장 위치를 저장할 수 없습니다", + "onboardingTitle": "Onboarding", + "onboardingHint": "다음 실행 때 onboarding wizard가 다시 나오도록 setup flag를 지웁니다.", + "resetConfirm": "저장된 키가 제거됩니다. 계속할까요?", + "reset": "초기화", + "resetButton": "Onboarding 초기화", + "pathsLoadFailed": "앱 경로를 불러오지 못했습니다", + "openFolderFailed": "폴더를 열 수 없습니다", + "onboardingResetToast": "Onboarding이 초기화되었습니다. 설정을 다시 실행하려면 앱을 재시작하세요.", + "diagnosticsTitle": "진단", + "diagnosticsHint": "로그 폴더를 열어 로그를 확인하거나 bug report용 redacted bundle을 내보내세요.", + "openLogFolder": "로그 폴더 열기", + "exportDiagnostics": "진단 내보내기", + "diagnosticsExported": "{{path}}로 내보냈습니다", + "diagnosticsExportFailed": "진단을 내보낼 수 없습니다" + }, + "diagnostics": { + "title": "진단", + "description": "최근 오류를 확인하세요. 전체 context를 첨부해 bug를 report할 수 있습니다.", + "openLogFolder": "로그 폴더 열기", + "exportBundle": "진단 bundle 내보내기", + "showTransient": "재시도 후 성공한 transient 오류 표시", + "empty": "아직 기록된 진단 event가 없습니다.", + "dbUnavailable": "진단 저장소를 사용할 수 없습니다. 오류는 main.log에 계속 기록되지만 앱 재시작 전까지 여기에 표시되지 않습니다. 디스크 공간과 권한을 확인하세요.", + "inMemoryFallback": "인메모리 기록을 표시 중입니다 — DB를 사용할 수 없어 재시작 후 유지되지 않습니다.", + "report": "보고", + "column": { + "time": "시간", + "code": "코드", + "scope": "범위", + "runId": "Run id", + "message": "메시지" + } + }, + "advanced": { + "updateChannel": "업데이트 채널", + "updateChannelHint": "Stable: 검증된 릴리스. Beta: 조기 접근(버그가 있을 수 있음).", + "stable": "Stable", + "beta": "Beta", + "checkForUpdatesOnStartup": "시작 시 업데이트 확인", + "checkForUpdatesOnStartupHint": "앱 실행 30초 후 새 릴리스를 자동 확인합니다.", + "timeout": "생성 timeout", + "timeoutHint": "생성 요청을 중단하기 전까지 기다릴 초 단위 시간입니다.", + "timeoutSeconds": "{{value}}초", + "devtools": "Developer tools", + "devtoolsHint": "renderer용 Chromium DevTools panel을 엽니다.", + "toggleDevtools": "DevTools 토글", + "prefsLoadFailed": "환경설정을 불러오지 못했습니다", + "prefsSaveFailed": "환경설정을 저장하지 못했습니다", + "devtoolsFailed": "Could not toggle DevTools" + } + }, + "updates": { + "bannerAvailable": "{{appName}} {{version}} is available.", + "bannerViewRelease": "View release", + "bannerDismissAria": "Dismiss update banner" + }, + "onboarding": { + "stepperLabel": "{{total}}단계 중 {{current}}단계", + "welcome": { + "title": "어떤 모델로든 디자인하세요.", + "subtitle": "디자인 생성에 사용할 방식을 선택하세요. 나중에 설정에서 바꿀 수 있습니다.", + "tryFree": "무료로 시작하기", + "tryFreeSubtitle": "OpenRouter 무료 티어 — OpenRouter 키를 붙여넣은 뒤 openrouter/free로 시작하거나 원하는 모델 ID를 입력하세요.", + "useKey": "내 API 키 사용", + "useKeySubtitle": "Anthropic, OpenAI, OpenRouter를 지원합니다. 키 접두사로 자동 감지합니다.", + "useOllama": "로컬 모델 사용 (Ollama)", + "useOllamaSubtitle": "API 비용 없이 100% 로컬에서 실행합니다. Ollama 설치 및 실행이 필요합니다.", + "useOllamaDetected": "Ollama 감지됨 — 로컬에서 {{count}}개 모델 사용 가능.", + "useOllamaNotRunning": "localhost:11434에서 Ollama를 찾지 못했습니다. 설치하거나 실행한 뒤 다시 시도하세요.", + "useOllamaProbing": "로컬 Ollama 인스턴스를 확인하는 중...", + "useOllamaRetry": "다시 확인", + "useOllamaInstall": "Ollama 설치 ↗", + "whereToGetKey": "키를 받는 방법" + }, + "paste": { + "title": "API 키 붙여넣기", + "description": "provider를 자동 감지합니다. 계속하기 전에 테스트를 눌러 키와 엔드포인트를 확인하세요. 키는 로컬 ~/.config/open-codesign/config.toml 파일(0600 권한)에 저장됩니다.", + "placeholder": "sk-…", + "recognized": "감지된 provider: {{provider}}", + "connected_one": "{{count}}개 모델 연결됨", + "connected_other": "{{count}}개 모델 연결됨", + "howToGet": "키를 받는 방법", + "getKey": "키 받기", + "statusIdle": "위에 키를 붙여넣으세요 — 접두사로 provider를 자동 감지합니다.", + "statusDetecting": "provider 감지 중...", + "statusValidating": "인식됨: {{provider}} — 검증 중...", + "statusDetected": "인식됨: {{provider}} — 테스트를 눌러 확인하세요", + "statusOk": "인식됨: {{provider}} — 연결됨 ({{count}}개 모델)", + "errors": { + "401": "API 키가 유효하지 않거나 권한이 없습니다. provider 대시보드에서 확인하세요.", + "402": "계정 크레딧이 없습니다. 충전 후 다시 시도하세요.", + "429": "요청 한도에 도달했습니다. 잠시 후 다시 시도하세요.", + "network": "Base URL에 연결할 수 없습니다. 도메인/포트/네트워크를 확인하세요.", + "unsupported": "인식할 수 없는 키 접두사입니다. 지원: sk-ant- (Anthropic), sk- (OpenAI), sk-or- (OpenRouter).", + "notSupportedProvider": "{{provider}}는 v0.1에서 지원하지 않습니다. Anthropic, OpenAI, OpenRouter를 사용하세요.", + "rendererDisconnected": "renderer가 main process에 연결되어 있지 않습니다.", + "detectIpc": "provider 감지 실패(main process 연결 불가): {{message}}. 앱을 재시작한 뒤 다시 시도하세요.", + "detectNetwork": "provider 감지 실패(네트워크 오류): {{message}}. 연결을 확인하고 다시 시도하세요." + }, + "advanced": { + "toggle": "고급 — custom base URL (proxy / relay)", + "title": "고급 — custom base URL", + "description": "provider의 기본 endpoint를 덮어씁니다. relay 서비스나 self-hosted proxy에 유용합니다. 공식 endpoint를 쓰려면 비워 두세요." + }, + "preset": { + "label": "Preset", + "placeholder": "-- preset 선택 --", + "hint": "무엇을 고를지 모르겠다면 공식 endpoint는 OpenAI Official을, relay는 이름에 맞는 항목을 선택하세요.", + "custom": "Custom" + }, + "apiKey": { + "label": "API 키" + }, + "baseUrl": { + "label": "Base URL" + }, + "connectionTest": { + "button": "테스트", + "testing": "테스트 중...", + "ok": "연결됨", + "okVerified": "연결됨 — 키와 endpoint 확인 완료", + "idleHint": "계속하기 전에 테스트를 실행해 키와 연결을 확인하세요.", + "runFirst": "먼저 테스트를 실행해 연결을 확인하세요", + "errors": { + "401": "API 키가 유효하지 않거나 권한이 없습니다.", + "404": "Base URL path가 잘못되었습니다. /v1 suffix를 추가해 보세요. 예: https://your-host/v1", + "ECONNREFUSED": "Base URL에 연결할 수 없습니다. 도메인/포트/네트워크를 확인하세요.", + "NETWORK": "네트워크 오류입니다. 연결을 확인하세요.", + "PARSE": "예상과 다른 응답입니다. ~/Library/Logs/open-codesign/main.log 로그를 확인하세요.", + "IPC_BAD_INPUT": "연결 테스트에 잘못된 입력이 전달되었습니다. provider / API 키 / Base URL 필드를 확인하세요." + } + }, + "back": "뒤로", + "continue": "계속" + }, + "choose": { + "title": "기본 모델 선택", + "description": "추천값으로 시작하거나 provider별 모델 ID를 직접 입력하세요. 나중에 디자인마다 전환할 수 있습니다.", + "primary": "주 디자인 모델", + "fast": "빠른 완성 모델", + "primaryHint": "전체 디자인 생성에 사용됩니다.", + "fastHint": "빠른 수정과 inline tweak에 사용됩니다.", + "primaryHintFree": "무료 경로는 openrouter/free로 시작하지만 원하는 OpenRouter 모델 ID를 입력할 수 있습니다.", + "fastHintFree": "최저 비용을 원하면 openrouter/free를 유지하거나 더 빠른 custom 선택지로 바꾸세요.", + "customBaseUrl": "Custom base URL: {{url}}", + "costNote": "예상 비용은 provider, 선택 모델, 프롬프트 길이에 따라 달라집니다.", + "costNoteFree": "OpenRouter 무료 routing availability는 바뀔 수 있습니다. 무료 route가 없으면 다른 모델 ID를 입력하세요.", + "estimatedCost": "예상 비용: {{amount}}", + "back": "뒤로", + "saving": "저장 중...", + "finish": "완료" + } + }, + "topbar": { + "modelSwitcher": { + "fromProvider": "Provider", + "searchPlaceholder": "Search models…", + "searchAriaLabel": "Filter models by name", + "clearSearch": "Clear search", + "noMatches": "No models match \"{{query}}\"" + }, + "status": { + "connected": "Connected", + "untested": "Not tested", + "error": "Connection error", + "noProvider": "No provider configured", + "lastTested": "Last tested {{time}}", + "tooltip": { + "click": "Click to re-test" + } + }, + "openMyDesigns": "All designs", + "hubLabel": "My designs", + "settingsLabel": "Settings", + "closeSettings": "Close settings", + "unreadErrors": "{{count}} unread error" + }, + "theme": { + "toggleAria": "Toggle theme", + "switchToLight": "Switch to light", + "switchToDark": "Switch to dark" + }, + "export": { + "button": "Export", + "items": { + "html": { + "label": "HTML", + "hint": "Single self-contained .html file" + }, + "pdf": { + "label": "PDF", + "hint": "Rendered via your installed Chrome" + }, + "pptx": { + "label": "PPTX", + "hint": "Editable slides; one per
" + }, + "zip": { + "label": "ZIP bundle", + "hint": "index.html + assets + README.md" + }, + "markdown": { + "label": "Markdown", + "hint": "Plain .md with YAML frontmatter" + } + } + }, + "notifications": { + "designSystemLinked": "Design system linked", + "designSystemScanFailed": "Design system scan failed", + "designSystemCleared": "Design system cleared", + "clearDesignSystemFailed": "Unable to clear design system", + "generationFailed": "Generation failed", + "generationFailedApplyFix": "Apply fix", + "generationFailedBaseUrlUpdated": "Base URL updated. Try the request again.", + "generationFailedFixUnavailable": "Auto-fix unavailable", + "generationFailedFixUnavailableDescription": "This app version can't apply the fix automatically. Please edit the Base URL manually in Settings.", + "generationFailedFixApplyFailed": "Could not apply fix", + "cancellationFailed": "Cancellation failed", + "inlineCommentFailed": "Inline comment failed", + "commentNeedsSnapshot": "Generate a design first before leaving a comment", + "commentCreateFailed": "Could not save the comment", + "commentUpdateFailed": "Could not update the comment", + "commentDeleteFailed": "Could not delete the comment", + "noDesignToExport": "No design to export yet.", + "exportedTo": "Exported to {{path}}" + }, + "inlineComment": { + "title": "Comment on", + "closeComposer": "Close inline comment composer", + "description": "Clicked elements stay selected in the canvas. Describe the visual or content change you want, and open-codesign will rewrite the artifact around that target.", + "placeholder": "Make this section more compact, sharpen the headline, and align it with the linked design system…", + "applying": "Applying…", + "applyChange": "Apply change" + }, + "commentBubble": { + "title": "Comment on", + "close": "Close comment bubble", + "placeholder": "Describe the change, or leave a note for yourself…", + "saveNote": "Comment", + "sendToClaude": "Send to Claude", + "saving": "Saving…", + "sending": "Sending…", + "scope": { + "legend": "Scope of this comment", + "element": "This element only", + "global": "Whole design" + } + }, + "pinOverlay": { + "note": "Note {{n}}", + "edit": "Edit {{n}}" + }, + "commentChip": { + "dismiss": "Dismiss pending edit", + "empty": "No pending edits", + "apply": "Apply ({{count}})", + "applyAll": "Apply all pending edits" + }, + "commentsTab": { + "empty": "Click any element in the preview while comment mode is on to leave a note or an edit instruction.", + "pendingEdits": "Pending edits", + "notes": "Notes", + "appliedEdits": "Applied edits", + "delete": "Delete comment", + "atSnapshot": "@ snapshot {{n}}" + }, + "comments": { + "panel": { + "title": "Comments", + "close": "Close comments panel", + "empty": "No comments yet. Click any element on the canvas to leave one.", + "delete": "Delete comment", + "untitled": "(no text)", + "status": { + "pending": "Pending", + "applied": "Applied" + }, + "scope": { + "element": "Element", + "global": "Global", + "tooltip": "Whether the change is scoped to this element or the whole design" + } + }, + "quickActions": { + "label": "Quick actions", + "spacing": { + "more": "+ Spacing", + "less": "− Spacing" + }, + "contrast": { + "more": "+ Contrast", + "less": "− Contrast" + }, + "font": { + "bigger": "+ Font", + "smaller": "− Font" + }, + "radius": { + "more": "+ Radius", + "less": "− Radius" + }, + "text": { + "spacing-more": "increase spacing on this element", + "spacing-less": "tighten spacing on this element", + "contrast-more": "increase color contrast", + "contrast-less": "soften the color contrast", + "font-bigger": "increase font size on this element", + "font-smaller": "decrease font size on this element", + "radius-more": "make corners more rounded", + "radius-less": "make corners sharper" + } + } + }, + "disabledReason": { + "typePromptToSend": "Type a prompt to start", + "generatingInProgress": "Generation in progress", + "typeDraftToApply": "Type a comment to apply", + "noDesignToExport": "Generate a design first", + "enterApiKeyToValidate": "Enter an API key to validate", + "validateKeyFirst": "Validate the key first", + "enterKeyToTest": "Paste an API key to test the connection", + "detectingProvider": "Detecting provider — please wait", + "unsupportedKeyForTest": "Unsupported key format — can't test this key", + "providerDetectIpcForTest": "Provider detection failed (IPC) — can't test this key", + "providerDetectNetworkForTest": "Provider detection failed (network) — can't test this key", + "testingConnection": "Testing connection…", + "validateKeyToContinue": "Validate your API key to continue", + "savingInProgress": "Saving in progress", + "enterBothModels": "Both model fields are required", + "ollamaComingSoon": "Ollama integration is coming in v0.2" + }, + "errorBoundary": { + "scopeFallback": "this view", + "crashedSuffix": "{{scope}} crashed", + "body": "The rest of the app is still running. Reload this view, or copy the stack to file a bug.", + "noStack": "(no stack)", + "copyStack": "Copy stack", + "reportOnGitHub": "Report on GitHub", + "reportViaDiagnostics": "Report via Diagnostics", + "reload": "Reload" + }, + "errors": { + "generic": "Something went wrong.", + "providerAuthMissing": "No API key configured. Open Settings to add one.", + "providerError": "Provider error: {{message}}", + "ipcBadInput": "Invalid input passed to the main process.", + "exporterNotReady": "Exporter is still loading. Try again in a moment.", + "rendererDisconnected": "Renderer is not connected to the main process.", + "onboardingIncomplete": "Onboarding is not complete.", + "providerMissingKey": "The active provider \"{{provider}}\" has no API key. Open Settings to add one.", + "modelListFailed": "Could not load model list.", + "unknown": "Unknown error", + "localePersistFailed": "Failed to save language preference" + }, + "projects": { + "untitled": "Untitled design", + "untitledNumbered": "Untitled design {{n}}", + "duplicateNameTemplate": "{{name}} copy", + "switcher": { + "currentLabel": "Current design", + "menuLabel": "Switch design", + "newDesign": "New design", + "renameCurrent": "Rename", + "viewAll": "View all designs…", + "recent": "Recent", + "noOthers": "No other designs yet" + }, + "view": { + "title": "Designs", + "subtitle": "All your saved designs. Switch, rename, duplicate, or delete.", + "newDesign": "New design", + "search": "Search designs", + "empty": "No designs yet — create your first one to get started.", + "noMatches": "No designs match \"{{query}}\".", + "open": "Open", + "rename": "Rename", + "duplicate": "Duplicate", + "delete": "Delete", + "edited": "Edited {{when}}", + "snapshotCount_one": "{{count}} version", + "snapshotCount_other": "{{count}} versions", + "close": "Close designs view" + }, + "rename": { + "title": "Rename design", + "label": "Design name", + "placeholder": "e.g. Landing page hero", + "save": "Save", + "cancel": "Cancel" + }, + "delete": { + "title": "Delete this design?", + "body": "\"{{name}}\" and all of its history will be removed from the designs list. This cannot be undone in v0.", + "confirm": "Delete design", + "cancel": "Keep it" + }, + "time": { + "justNow": "just now", + "minutesAgo_one": "{{count}} minute ago", + "minutesAgo_other": "{{count}} minutes ago", + "hoursAgo_one": "{{count}} hour ago", + "hoursAgo_other": "{{count}} hours ago", + "yesterday": "yesterday", + "daysAgo_one": "{{count}} day ago", + "daysAgo_other": "{{count}} days ago" + }, + "notifications": { + "createFailed": "Could not create a new design", + "switchFailed": "Could not switch design", + "switchBlockedGenerating": "Wait for generation to finish before switching", + "busyGenerating": "Wait for the current generation to finish, then try again", + "renameFailed": "Could not rename the design", + "duplicated": "Duplicated as \"{{name}}\"", + "duplicateFailed": "Could not duplicate the design", + "deleted": "Design moved out of the active list", + "deleteFailed": "Could not delete the design", + "deleteBlockedGenerating": "Wait for generation to finish before deleting", + "saveFailed": "Could not save the latest changes", + "snapshotSkipped": "Incomplete output kept unsaved", + "snapshotSkippedBody": "The agent's JSX is truncated (missing ReactDOM.createRoot or unbalanced braces). Previous good version preserved. Send the prompt again to retry.", + "loadFailed": "Could not load your designs" + } + }, + "diagnostics": { + "title": "Connection failed", + "status": "Status: {{status}}", + "attempted": "What we tried: {{url}}", + "mostLikelyCause": "Most likely cause:", + "cause": { + "keyInvalid": "API key invalid or revoked.", + "balanceEmpty": "Account balance is empty.", + "missingV1": "Base URL path is likely missing the /v1 suffix.", + "rateLimit": "Rate limit exceeded.", + "hostUnreachable": "Cannot reach host — check domain, port, or VPN.", + "timedOut": "Request timed out — check firewall or VPN.", + "corsError": "CORS error (should not happen in main process). This is a bug.", + "sslError": "SSL / certificate error (self-signed cert on relay?).", + "gatewayIncompatible": "The gateway accepted the connection but does not implement this provider's API. Try switching wire (e.g. openai-chat).", + "openaiResponsesMisconfigured": "The endpoint rejected the request shape. The wire may be wrong — try switching to openai-chat.", + "relayStreamingBug": "The gateway may mishandle OpenAI Responses API SSE events (older sub2api / claude2api / anyrouter builds cut the stream short).", + "serverError": "Upstream server error. May be transient — try again.", + "unknown": "Unknown error — check the full log for details." + }, + "fix": { + "updateKey": "Update key", + "addCredits": "Add credits →", + "addCreditsGeneric": "Check your provider's billing page", + "addV1": "Add /v1", + "waitAndRetry": "Wait and retry", + "checkNetwork": "Check network / VPN", + "checkVpn": "Check VPN / firewall", + "reportBug": "Report this bug", + "disableTls": "Disable TLS verify", + "switchWire": "Switch wire in Settings", + "relayStreamingBug": "Upgrade the relay, switch wire to openai-chat, or use api.openai.com directly" + }, + "applyFix": "Apply this fix", + "setBaseUrlFirst": "Set a Base URL first", + "testAgain": "Test again", + "showLog": "Show full log", + "showLogFailed": "Failed to open logs folder", + "gatewayAllowlistHintTitle": "This endpoint may restrict which apps can connect", + "gatewayAllowlistHintBody": "Some coding plans and protocol-conversion gateways keep an app allowlist. Even when they advertise an OpenAI-compatible API, they may still reject Open CoDesign unless this app is explicitly allowed.", + "dismiss": "Dismiss", + "report": { + "title": "Report a bug", + "notes": "Steps to reproduce (optional)", + "include": { + "prompt": "Include prompt text", + "paths": "Include file paths", + "urls": "Include URLs", + "timeline": "Include 60 s action timeline", + "promptHint": "Includes the text of prompts you submitted during the session.", + "pathsHint": "Includes local file paths (can reveal your username / directory structure).", + "urlsHint": "Includes reference URLs you pasted.", + "timelineHint": "60-second activity log — actions only, no content." + }, + "disclaimer": "Nothing is uploaded automatically. The diagnostic bundle is saved to your Downloads folder; attach it to the GitHub issue manually.", + "openIssue": "Open issue", + "copySummary": "Copy summary", + "cancel": "Cancel", + "copied": "Copied to clipboard", + "generating": "Creating bundle…", + "close": "Close", + "scope": "Scope", + "runId": "Run id", + "fingerprint": "Fingerprint", + "message": "Message", + "recentlyReported": "You reported this {{relative}} as issue #{{issueNumber}}.", + "recentlyReportedNoNumber": "You reported this {{relative}}.", + "viewPrevious": "View previous issue", + "continueAnyway": "Continue anyway", + "confirmOpenAnyway": "Yes, open anyway ({{seconds}}s)", + "error": { + "notesTooLong": "Notes exceed the 2000-character limit.", + "generic": "Couldn't open the issue. Check the log folder for details." + }, + "bundleSavedTitle": "Bundle saved", + "bundleSavedDescription": "Saved to", + "revealBundle": "Show in folder", + "openFailedTitle": "Couldn't open the browser", + "openFailedCopyHint": "Copy the issue URL and open it manually.", + "copyIssueUrl": "Copy URL", + "clipboardFailedTitle": "Couldn't copy to clipboard", + "clipboardFailedHint": "The bundle is on disk — open it manually and paste the summary there.", + "preview": { + "code": "Code", + "scope": "Scope", + "runId": "Run id", + "fingerprint": "Fingerprint", + "message": "Message", + "upstream": "Upstream context", + "upstreamProvider": "Provider", + "upstreamStatus": "Status", + "upstreamRequestId": "Request id", + "upstreamRetry": "Retry", + "upstreamBodyHead": "Body head" + } + } + }, + "demos": { + "meditationApp": { + "title": "Calm Spaces meditation app", + "description": "Mobile prototype with phone frame, soft palette, interactive nav.", + "prompt": "Design a mobile app prototype for a meditation app called Calm Spaces. Show a phone frame containing a home screen with a meditation list, play button, and progress tracker. Use serene typography, soft greens and blues, and lots of white space." + }, + "caseStudy": { + "title": "Client case study one-pager", + "description": "Dark theme one-page PDF-ready layout with hero metrics.", + "prompt": "Create a one-page client case study. The client increased qualified leads 40% using our platform. Include before/after metrics, a CEO quote, and a logo placeholder. Clean, minimal, dark theme." + }, + "pitchDeck": { + "title": "B2B SaaS pitch deck", + "description": "8-12 slides for a healthcare-targeted SaaS pitch.", + "prompt": "Design a pitch deck for a B2B SaaS company targeting mid-market healthcare. 8 to 10 slides covering problem, market, product, traction, team, and ask." + }, + "marketingLanding": { + "title": "Marketing landing page", + "description": "Hero + features + CTA, tunable accent color.", + "prompt": "Design a modern marketing landing page for an AI productivity tool. Include a hero section, three feature cards, social proof, and a call to action. Use a warm neutral palette." + } + }, + "examples": { + "tab": "Examples", + "title": "Examples", + "subtitle": "Hand-picked starting points. Hover to preview, then drop the prompt into the composer.", + "empty": "No examples match your filter.", + "useThisPrompt": "Use this prompt", + "categories": { + "all": "All", + "animation": "Animation", + "ui": "UI", + "marketing": "Marketing", + "document": "Document", + "dashboard": "Dashboard", + "presentation": "Presentation", + "email": "Email", + "mobile": "Mobile" + }, + "thumbnailAlt": "{{title}} preview", + "promptUsedToast": "Prompt loaded — edit and send when ready." + }, + "emptyState": { + "heading": "What would you like to design?", + "subline": "Describe your idea in the chat, or pick a template to get started.", + "tryThese": "Quick starts", + "starters": { + "landing": "Startup landing page", + "pitch": "Pitch deck slides", + "mobile": "Mobile onboarding", + "dashboard": "Analytics dashboard", + "email": "Welcome email", + "portfolio": "Photo portfolio", + "casestudy": "Case study page", + "animation": "CSS animation showcase" + }, + "starterDesc": { + "landing": "Hero + feature cards + social proof + CTA", + "pitch": "Title + problem + solution, 16:9 slides", + "mobile": "3-screen flow inside a phone frame", + "dashboard": "Line chart + bars + table, dark theme", + "email": "600px single-column with onboarding steps", + "portfolio": "Masonry grid + filters + hover overlays", + "casestudy": "Key metrics + quote + implementation steps", + "animation": "Pure CSS/SVG animations, 60fps smooth" + } + }, + "starterPrompts": { + "landing": "Build a landing page for an AI startup. Hero with a strong tagline, 3 feature cards, social proof section, CTA. Editorial typography, generous whitespace.", + "pitch": "Generate the first 3 slides of a pitch deck for a fintech startup: title slide, problem, solution. 16:9 format, navy palette, one orange accent.", + "mobile": "Design a 3-screen mobile app onboarding flow inside a phone frame: welcome, permissions, first action. Soft mint palette.", + "dashboard": "Build an analytics dashboard with: MRR trend line chart, pipeline stacked bars, top accounts table, and forecast gauge. Dark theme, teal + amber accents, plausible mock data.", + "email": "Design a transactional welcome email for a SaaS product. Single column, 600px wide, table-based. Logo header, greeting, 3 onboarding steps, CTA button, footer.", + "portfolio": "Design a photographer portfolio with masonry image grid using CSS gradient placeholders. Dark background, category filter pills, hover overlay with title and camera settings.", + "casestudy": "Create a one-page customer case study for a B2B fintech. Hero with client name, three large metrics with deltas, a CFO pull quote, a three-step 'How we did it' section, and a logo strip. Dark theme, serif headings, monospace numerals.", + "animation": "Build a showcase page with six organic CSS loading animations. Each in its own card: blob morph, leaf sway, ink drop, breathing circle, soft pulse, ribbon weave. Warm cream background, muted pastels, pure CSS/SVG only." + }, + "hub": { + "newDesign": "New design", + "newDesignCardTitle": "Start a new design", + "newDesignCardSub": "Blank canvas · AI generated", + "backToHub": "All designs", + "tabs": { + "recent": "Recent", + "your": "Your designs", + "examples": "Examples", + "designSystems": "Design systems" + }, + "recent": { + "title": "Recent", + "empty": "Nothing here yet — start a new design to see it pinned at the top." + }, + "your": { + "title": "Your designs", + "empty": "No designs saved yet. Click \"New design\" to create your first one.", + "openAria": "Open {{name}}" + }, + "examples": { + "title": "Examples", + "comingSoon": "A curated gallery of starter prompts is on the way." + }, + "designSystems": { + "title": "Design systems", + "comingSoon": "Reusable brand systems and templates are on the way." + }, + "card": { + "type": { + "prototype": "Prototype", + "slideDeck": "Slide deck", + "template": "From template", + "other": "Other" + }, + "createdAt": "Created {{date}}", + "moreActions": "More actions for {{name}}", + "rename": "Rename", + "delete": "Delete" + } + }, + "create": { + "title": "Start a new design", + "subtitle": "Pick a type so we can tailor the first prompt.", + "close": "Close", + "types": { + "prototype": "Prototype", + "slideDeck": "Slide deck", + "template": "From template", + "other": "Other" + }, + "typeDescriptions": { + "prototype": "Interactive UI mockups with a phone or browser frame.", + "slideDeck": "A multi-slide deck for talks, pitches, or summaries.", + "template": "Start from one of the bundled starter prompts.", + "other": "A blank canvas — describe anything else you have in mind." + }, + "fields": { + "name": "Project name", + "namePlaceholder": "Untitled design", + "fidelity": "Fidelity", + "fidelityWireframe": "Wireframe", + "fidelityWireframeHint": "Greyscale blocks, no imagery — quick exploration.", + "fidelityHigh": "High-fidelity", + "fidelityHighHint": "Polished colors, real-looking content.", + "fidelityComingSoon": "Wireframe vs. high-fidelity selection ships in a follow-up.", + "speakerNotes": "Include speaker notes", + "speakerNotesHint": "Adds a notes section under each slide for delivery cues.", + "template": "Template", + "templateHint": "Choose a starter — the first prompt will be pre-filled." + }, + "cta": "Create", + "disabledHint": "Add a project name to continue.", + "help": { + "share": "Designs stay on this device. Sharing happens through bundle export — no cloud account needed.", + "templates": "More starters arrive in a follow-up; this list will keep growing." + } + }, + "err": { + "IPC_BAD_INPUT": "The request contained invalid input. Please try again.", + "IPC_DB_ERROR": "A local database error occurred. Restarting the app may help.", + "IPC_NOT_FOUND": "The requested item was not found.", + "PROVIDER_AUTH_MISSING": "No API key found for this provider. Please add your key in Settings.", + "PROVIDER_KEY_MISSING": "No API key is stored for this provider. Add one in Settings.", + "PROVIDER_ACTIVE_MISSING_KEY": "The active provider has no API key. Open Settings to add one.", + "PROVIDER_NOT_SUPPORTED": "This provider is not supported. Check your provider configuration.", + "PROVIDER_MODEL_UNKNOWN": "The selected model is not available for this provider.", + "PROVIDER_BASE_URL_MISSING": "A base URL is required for this provider. Configure it in Settings.", + "PROVIDER_ERROR": "The provider returned an error. Check your API key and try again.", + "PROVIDER_HTTP_4XX": "The provider rejected the request. Verify your API key and billing.", + "PROVIDER_UPSTREAM_ERROR": "The provider returned an unexpected error. Details are in the log.", + "PROVIDER_GATEWAY_INCOMPATIBLE": "Your gateway returned 'not implemented' for the Messages API. Try switching wire to openai-chat in Settings, or use a gateway that supports the Anthropic Messages API.", + "PROVIDER_ABORTED": "Generation was cancelled.", + "PROVIDER_RETRY_EXHAUSTED": "The provider failed after several retries. Check your connection and try again.", + "CLAUDE_CODE_OAUTH_ONLY": "Your Claude Code login uses an Anthropic subscription (Pro/Max). Third-party apps cannot reuse the subscription quota — generate an API key at console.anthropic.com and use it here.", + "CODEX_TOKEN_PARSE_FAILED": "Local ChatGPT login is corrupted. Please re-login in Settings.", + "CODEX_TOKEN_NOT_LOGGED_IN": "ChatGPT subscription is not signed in. Please log in via Settings.", + "INPUT_EMPTY_PROMPT": "The prompt cannot be empty.", + "INPUT_EMPTY_COMMENT": "The comment cannot be empty.", + "INPUT_EMPTY_HTML": "Existing HTML is required for this operation.", + "INPUT_UNSUPPORTED_MODE": "This generation mode is not supported.", + "GENERATION_TIMEOUT": "Generation timed out. Try a shorter prompt or increase the timeout in Settings.", + "CONFIG_READ_FAILED": "Failed to read configuration file. Check file permissions.", + "CONFIG_PARSE_FAILED": "Configuration file could not be parsed. It may be corrupt.", + "CONFIG_SCHEMA_INVALID": "Configuration file has an unrecognised format. Please reconfigure.", + "CONFIG_NOT_LOADED": "Configuration has not been loaded yet. Please restart the app.", + "CONFIG_MISSING": "No configuration found. Complete onboarding to get started.", + "SNAPSHOTS_UNAVAILABLE": "The local design database is unavailable. Restarting the app may help.", + "BOOT_ORDER": "An internal startup error occurred. Please restart the app.", + "STORAGE_SETTINGS_READ_FAILED": "Failed to read storage location settings.", + "STORAGE_SETTINGS_PARSE_FAILED": "Storage location settings could not be parsed.", + "STORAGE_SETTINGS_INVALID": "Storage location settings contain invalid data.", + "KEYCHAIN_UNAVAILABLE": "OS keychain (secure storage) is not available. API keys cannot be stored.", + "KEYCHAIN_EMPTY_INPUT": "Cannot encrypt or decrypt an empty value.", + "ATTACHMENT_TOO_LARGE": "One or more attachments exceed the size limit.", + "ATTACHMENT_READ_FAILED": "Failed to read an attachment file. Check that the file still exists.", + "REFERENCE_URL_TOO_LARGE": "The reference URL content is too large to include.", + "REFERENCE_URL_FETCH_FAILED": "Could not fetch the reference URL. Check the URL and your internet connection.", + "REFERENCE_URL_FETCH_TIMEOUT": "Fetching the reference URL timed out. Try again or use a different URL.", + "REFERENCE_URL_UNSUPPORTED": "This type of reference URL is not supported.", + "PREFERENCES_READ_FAIL": "Failed to read preferences. Default settings will be used.", + "PREFERENCES_INVALID_TIMEOUT": "The generation timeout value is invalid.", + "SKILL_LOAD_FAILED": "One or more skills failed to load. Check your skill files for errors.", + "EXPORTER_UNKNOWN": "Unknown export format requested.", + "EXPORTER_NO_CHROME": "Chrome or Chromium was not found. Install it to enable PDF export.", + "EXPORTER_PDF_FAILED": "PDF export failed. Ensure Chrome is installed and try again.", + "EXPORTER_PPTX_FAILED": "PowerPoint export failed.", + "EXPORTER_ZIP_UNSAFE_PATH": "Export was blocked: an asset path would escape the ZIP archive.", + "EXPORTER_ZIP_FAILED": "ZIP export failed.", + "OPEN_PATH_FAILED": "Could not open the requested folder or file.", + "RENDERER_ERROR": "An error occurred in the renderer. Details are in the log." + } +} diff --git a/packages/i18n/src/locales/pt-BR.json b/packages/i18n/src/locales/pt-BR.json index c605e180..771b6249 100644 --- a/packages/i18n/src/locales/pt-BR.json +++ b/packages/i18n/src/locales/pt-BR.json @@ -362,7 +362,8 @@ "languageLoadFailed": "Falha ao carregar o idioma", "langEn": "English", "langZhCN": "中文 (简体)", - "langPtBR": "Português (BR)" + "langPtBR": "Português (BR)", + "langKo": "한국어" }, "language": { "label": "Idioma", diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json index c13b628a..5353a04c 100644 --- a/packages/i18n/src/locales/zh-CN.json +++ b/packages/i18n/src/locales/zh-CN.json @@ -397,7 +397,8 @@ "languageLoadFailed": "加载语言设置失败", "langEn": "English", "langZhCN": "中文 (简体)", - "langPtBR": "Português (BR)" + "langPtBR": "Português (BR)", + "langKo": "한국어" }, "language": { "label": "语言", diff --git a/packages/templates/src/examples/index.ts b/packages/templates/src/examples/index.ts index 100e70ad..344eae63 100644 --- a/packages/templates/src/examples/index.ts +++ b/packages/templates/src/examples/index.ts @@ -217,6 +217,7 @@ const REGISTRY: Record> = { en: enExamples, 'zh-CN': zhCNExamples, 'pt-BR': ptBRExamples, + ko: enExamples, }; function getRegistry(locale: string | undefined): Record { diff --git a/packages/templates/src/index.ts b/packages/templates/src/index.ts index 461baea3..f0d18085 100644 --- a/packages/templates/src/index.ts +++ b/packages/templates/src/index.ts @@ -34,6 +34,7 @@ const REGISTRY: Record = { en: enDemos, 'zh-CN': zhCNDemos, 'pt-BR': ptBRDemos, + ko: enDemos, }; export function getDemos(locale: string | undefined): DemoTemplate[] {