Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/ko-locale-core.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/src/components/LanguageToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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';
}

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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') },
]}
/>
</Row>
Expand Down
4 changes: 3 additions & 1 deletion packages/i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions packages/i18n/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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');
Expand Down Expand Up @@ -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');
});
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
7 changes: 6 additions & 1 deletion packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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}". ` +
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@
"languageLoadFailed": "Failed to load language",
"langEn": "English",
"langZhCN": "中文 (简体)",
"langPtBR": "Português (BR)"
"langPtBR": "Português (BR)",
"langKo": "한국어"
},
"language": {
"label": "Language",
Expand Down
Loading
Loading