Add Korean UI localization (#2895)
* Add Korean UI localization Refreshes the Korean translation from #1544 against the current client locale surface and adds cross-platform locale detection coverage.\n\nCo-authored-by: JinHyeok Jeong <150323322+Kesta-bos@users.noreply.github.com> * fix(i18n): preserve reviewed Korean status labels Restore the wording corrections contributed in the original Korean localization work and lock them with exact-copy assertions. Co-authored-by: JinHyeok Jeong <150323322+Kesta-bos@users.noreply.github.com> --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> Co-authored-by: JinHyeok Jeong <150323322+Kesta-bos@users.noreply.github.com>
This commit is contained in:
co-authored by
Mohamed Boudra
JinHyeok Jeong
parent
94b57f34ad
commit
0728148657
+4
-2
@@ -9,11 +9,12 @@ Paseo client UI translations live in `packages/app/src/i18n`.
|
|||||||
- `es`
|
- `es`
|
||||||
- `fr`
|
- `fr`
|
||||||
- `ja`
|
- `ja`
|
||||||
|
- `ko`
|
||||||
- `pt-BR`
|
- `pt-BR`
|
||||||
- `ru`
|
- `ru`
|
||||||
- `zh-CN`
|
- `zh-CN`
|
||||||
|
|
||||||
The persisted app language setting is `"system" | "ar" | "en" | "es" | "fr" | "ja" | "pt-BR" | "ru" | "zh-CN"`. `"system"` follows the device or browser locale when it maps to a supported locale; unsupported system locales fall back to English. Japanese maps from system locales `ja` and Japanese regional locales. Brazilian Portuguese maps from system locales `pt-BR` and bare `pt`; other Portuguese regional locales remain unsupported until explicitly added.
|
The persisted app language setting is `"system" | "ar" | "en" | "es" | "fr" | "ja" | "ko" | "pt-BR" | "ru" | "zh-CN"`. `"system"` follows the device or browser locale when it maps to a supported locale; unsupported system locales fall back to English. Japanese maps from system locales `ja` and Japanese regional locales. Korean maps from system locales `ko` and Korean regional locales. Brazilian Portuguese maps from system locales `pt-BR` and bare `pt`; other Portuguese regional locales remain unsupported until explicitly added.
|
||||||
|
|
||||||
## Translation Scope
|
## Translation Scope
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ Do not translate agent output, daemon output, terminal contents, file paths, pro
|
|||||||
|
|
||||||
## Adding Copy
|
## Adding Copy
|
||||||
|
|
||||||
English source strings live in `packages/app/src/i18n/resources/en.ts`. Simplified Chinese strings live in `packages/app/src/i18n/resources/zh-CN.ts`.
|
English source strings live in `packages/app/src/i18n/resources/en.ts`. Each supported locale has a matching resource in `packages/app/src/i18n/resources`.
|
||||||
|
|
||||||
For migrated screens and components, use `useTranslation()` and pass translated text into UI primitives. Low-level primitives such as `<Button>` do not import translation state unless they own the text they render.
|
For migrated screens and components, use `useTranslation()` and pass translated text into UI primitives. Low-level primitives such as `<Button>` do not import translation state unless they own the text they render.
|
||||||
|
|
||||||
@@ -89,3 +90,4 @@ Within a migrated surface, do not leave mixed-language neighboring labels when t
|
|||||||
- Batch 4Y tightened the translation boundary so React components and custom hooks use `useTranslation()` while pure helpers keep direct `i18n.t(...)` fallbacks, and migrated remaining small UI/accessibility fallbacks across message details, menu backdrops, startup errors, sidebar PR badges, settings/project accessibility labels, composer send/create/download fallbacks, client slash-command descriptions, terminal subscribe errors, and desktop update completion text. Provider catalog metadata, shortcut registry fallbacks, agent/daemon/protocol reasons, terminal contents, raw runtime errors, and user/project/workspace names remain untranslated.
|
- Batch 4Y tightened the translation boundary so React components and custom hooks use `useTranslation()` while pure helpers keep direct `i18n.t(...)` fallbacks, and migrated remaining small UI/accessibility fallbacks across message details, menu backdrops, startup errors, sidebar PR badges, settings/project accessibility labels, composer send/create/download fallbacks, client slash-command descriptions, terminal subscribe errors, and desktop update completion text. Provider catalog metadata, shortcut registry fallbacks, agent/daemon/protocol reasons, terminal contents, raw runtime errors, and user/project/workspace names remain untranslated.
|
||||||
- Batch 4Z expanded the supported locale set to the six UN official languages: Arabic, Chinese, English, French, Russian, and Spanish. Arabic, French, Russian, and Spanish now have full client-owned UI resource coverage, with key parity, fallback-ratio, and interpolation-placeholder tests guarding the generated translations. Arabic does not enable RTL layout direction in this batch.
|
- Batch 4Z expanded the supported locale set to the six UN official languages: Arabic, Chinese, English, French, Russian, and Spanish. Arabic, French, Russian, and Spanish now have full client-owned UI resource coverage, with key parity, fallback-ratio, and interpolation-placeholder tests guarding the generated translations. Arabic does not enable RTL layout direction in this batch.
|
||||||
- Batch 5A added Brazilian Portuguese (`pt-BR`) resource coverage, language selector labels, i18next registration, and system-locale mapping for `pt-BR` and bare `pt`. Non-Brazilian Portuguese regional locales remain unsupported until a matching resource is added.
|
- Batch 5A added Brazilian Portuguese (`pt-BR`) resource coverage, language selector labels, i18next registration, and system-locale mapping for `pt-BR` and bare `pt`. Non-Brazilian Portuguese regional locales remain unsupported until a matching resource is added.
|
||||||
|
- Batch 5B added Korean (`ko`) resource coverage, language selector labels, i18next registration, and system-locale mapping for `ko` and Korean regional locales.
|
||||||
|
|||||||
@@ -21,3 +21,21 @@ test("Settings language selector switches General labels", async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
|
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Settings language selector switches to Korean", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await openSettings(page);
|
||||||
|
await openSettingsSection(page, "general");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "System", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "한국어 - Korean", exact: true }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("기본 전송", { exact: true }).first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "한국어", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "English - 영어", exact: true }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { en } from "./resources/en";
|
|||||||
import { es } from "./resources/es";
|
import { es } from "./resources/es";
|
||||||
import { fr } from "./resources/fr";
|
import { fr } from "./resources/fr";
|
||||||
import { ja } from "./resources/ja";
|
import { ja } from "./resources/ja";
|
||||||
|
import { ko } from "./resources/ko";
|
||||||
import { ptBR } from "./resources/pt-BR";
|
import { ptBR } from "./resources/pt-BR";
|
||||||
import { ru } from "./resources/ru";
|
import { ru } from "./resources/ru";
|
||||||
import { zhCN } from "./resources/zh-CN";
|
import { zhCN } from "./resources/zh-CN";
|
||||||
@@ -23,6 +24,7 @@ observeI18nInit(
|
|||||||
es: { translation: es },
|
es: { translation: es },
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
ja: { translation: ja },
|
ja: { translation: ja },
|
||||||
|
ko: { translation: ko },
|
||||||
"pt-BR": { translation: ptBR },
|
"pt-BR": { translation: ptBR },
|
||||||
ru: { translation: ru },
|
ru: { translation: ru },
|
||||||
"zh-CN": { translation: zhCN },
|
"zh-CN": { translation: zhCN },
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
describe("parseAppLanguage", () => {
|
describe("parseAppLanguage", () => {
|
||||||
it("accepts system and all supported language locales", () => {
|
it("accepts system and all supported language locales", () => {
|
||||||
expect(
|
expect(
|
||||||
["system", "ar", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN"].map(parseAppLanguage),
|
["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"].map(parseAppLanguage),
|
||||||
).toEqual(["system", "ar", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN"]);
|
).toEqual(["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for unknown values", () => {
|
it("returns null for unknown values", () => {
|
||||||
@@ -26,6 +26,7 @@ describe("parseAppLanguage", () => {
|
|||||||
"es",
|
"es",
|
||||||
"fr",
|
"fr",
|
||||||
"ja",
|
"ja",
|
||||||
|
"ko",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
"ru",
|
"ru",
|
||||||
"zh-CN",
|
"zh-CN",
|
||||||
@@ -37,6 +38,7 @@ describe("formatLanguageOptionLabel", () => {
|
|||||||
it("shows the native language name and English name in English UI", () => {
|
it("shows the native language name and English name in English UI", () => {
|
||||||
const arabic = LANGUAGE_OPTIONS.find((option) => option.value === "ar");
|
const arabic = LANGUAGE_OPTIONS.find((option) => option.value === "ar");
|
||||||
const japanese = LANGUAGE_OPTIONS.find((option) => option.value === "ja");
|
const japanese = LANGUAGE_OPTIONS.find((option) => option.value === "ja");
|
||||||
|
const korean = LANGUAGE_OPTIONS.find((option) => option.value === "ko");
|
||||||
const portuguese = LANGUAGE_OPTIONS.find((option) => option.value === "pt-BR");
|
const portuguese = LANGUAGE_OPTIONS.find((option) => option.value === "pt-BR");
|
||||||
const spanish = LANGUAGE_OPTIONS.find((option) => option.value === "es");
|
const spanish = LANGUAGE_OPTIONS.find((option) => option.value === "es");
|
||||||
const chinese = LANGUAGE_OPTIONS.find((option) => option.value === "zh-CN");
|
const chinese = LANGUAGE_OPTIONS.find((option) => option.value === "zh-CN");
|
||||||
@@ -44,12 +46,14 @@ describe("formatLanguageOptionLabel", () => {
|
|||||||
expect([
|
expect([
|
||||||
formatLanguageOptionLabel(arabic!, "en", "System"),
|
formatLanguageOptionLabel(arabic!, "en", "System"),
|
||||||
formatLanguageOptionLabel(japanese!, "en", "System"),
|
formatLanguageOptionLabel(japanese!, "en", "System"),
|
||||||
|
formatLanguageOptionLabel(korean!, "en", "System"),
|
||||||
formatLanguageOptionLabel(portuguese!, "en", "System"),
|
formatLanguageOptionLabel(portuguese!, "en", "System"),
|
||||||
formatLanguageOptionLabel(spanish!, "en", "System"),
|
formatLanguageOptionLabel(spanish!, "en", "System"),
|
||||||
formatLanguageOptionLabel(chinese!, "en", "System"),
|
formatLanguageOptionLabel(chinese!, "en", "System"),
|
||||||
]).toEqual([
|
]).toEqual([
|
||||||
"العربية - Arabic",
|
"العربية - Arabic",
|
||||||
"日本語 - Japanese",
|
"日本語 - Japanese",
|
||||||
|
"한국어 - Korean",
|
||||||
"Português brasileiro - Brazilian Portuguese",
|
"Português brasileiro - Brazilian Portuguese",
|
||||||
"Español - Spanish",
|
"Español - Spanish",
|
||||||
"简体中文 - Simplified Chinese",
|
"简体中文 - Simplified Chinese",
|
||||||
@@ -71,10 +75,12 @@ describe("formatLanguageOptionLabel", () => {
|
|||||||
it("uses a single label when both language names match", () => {
|
it("uses a single label when both language names match", () => {
|
||||||
const english = LANGUAGE_OPTIONS.find((option) => option.value === "en");
|
const english = LANGUAGE_OPTIONS.find((option) => option.value === "en");
|
||||||
const japanese = LANGUAGE_OPTIONS.find((option) => option.value === "ja");
|
const japanese = LANGUAGE_OPTIONS.find((option) => option.value === "ja");
|
||||||
|
const korean = LANGUAGE_OPTIONS.find((option) => option.value === "ko");
|
||||||
const portuguese = LANGUAGE_OPTIONS.find((option) => option.value === "pt-BR");
|
const portuguese = LANGUAGE_OPTIONS.find((option) => option.value === "pt-BR");
|
||||||
|
|
||||||
expect(formatLanguageOptionLabel(english!, "en", "System")).toBe("English");
|
expect(formatLanguageOptionLabel(english!, "en", "System")).toBe("English");
|
||||||
expect(formatLanguageOptionLabel(japanese!, "ja", "システム")).toBe("日本語");
|
expect(formatLanguageOptionLabel(japanese!, "ja", "システム")).toBe("日本語");
|
||||||
|
expect(formatLanguageOptionLabel(korean!, "ko", "시스템")).toBe("한국어");
|
||||||
expect(formatLanguageOptionLabel(portuguese!, "pt-BR", "Sistema")).toBe("Português brasileiro");
|
expect(formatLanguageOptionLabel(portuguese!, "pt-BR", "Sistema")).toBe("Português brasileiro");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,6 +98,7 @@ describe("resolveSupportedLocale", () => {
|
|||||||
expect(resolveSupportedLocale("es", ["en-US"])).toBe("es");
|
expect(resolveSupportedLocale("es", ["en-US"])).toBe("es");
|
||||||
expect(resolveSupportedLocale("fr", ["en-US"])).toBe("fr");
|
expect(resolveSupportedLocale("fr", ["en-US"])).toBe("fr");
|
||||||
expect(resolveSupportedLocale("ja", ["en-US"])).toBe("ja");
|
expect(resolveSupportedLocale("ja", ["en-US"])).toBe("ja");
|
||||||
|
expect(resolveSupportedLocale("ko", ["en-US"])).toBe("ko");
|
||||||
expect(resolveSupportedLocale("pt-BR", ["en-US"])).toBe("pt-BR");
|
expect(resolveSupportedLocale("pt-BR", ["en-US"])).toBe("pt-BR");
|
||||||
expect(resolveSupportedLocale("ru", ["en-US"])).toBe("ru");
|
expect(resolveSupportedLocale("ru", ["en-US"])).toBe("ru");
|
||||||
expect(resolveSupportedLocale("zh-CN", ["en-US"])).toBe("zh-CN");
|
expect(resolveSupportedLocale("zh-CN", ["en-US"])).toBe("zh-CN");
|
||||||
@@ -103,11 +110,18 @@ describe("resolveSupportedLocale", () => {
|
|||||||
expect(resolveSupportedLocale("system", ["es-MX"])).toBe("es");
|
expect(resolveSupportedLocale("system", ["es-MX"])).toBe("es");
|
||||||
expect(resolveSupportedLocale("system", ["fr-CA"])).toBe("fr");
|
expect(resolveSupportedLocale("system", ["fr-CA"])).toBe("fr");
|
||||||
expect(resolveSupportedLocale("system", ["ja-JP"])).toBe("ja");
|
expect(resolveSupportedLocale("system", ["ja-JP"])).toBe("ja");
|
||||||
|
expect(resolveSupportedLocale("system", ["ko-KR"])).toBe("ko");
|
||||||
expect(resolveSupportedLocale("system", ["pt-BR"])).toBe("pt-BR");
|
expect(resolveSupportedLocale("system", ["pt-BR"])).toBe("pt-BR");
|
||||||
expect(resolveSupportedLocale("system", ["pt"])).toBe("pt-BR");
|
expect(resolveSupportedLocale("system", ["pt"])).toBe("pt-BR");
|
||||||
expect(resolveSupportedLocale("system", ["ru-RU"])).toBe("ru");
|
expect(resolveSupportedLocale("system", ["ru-RU"])).toBe("ru");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps Korean system locales to Korean", () => {
|
||||||
|
expect(resolveSupportedLocale("system", ["ko"])).toBe("ko");
|
||||||
|
expect(resolveSupportedLocale("system", ["ko-KR"])).toBe("ko");
|
||||||
|
expect(resolveSupportedLocale("system", ["ko-KP"])).toBe("ko");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not map non-Brazilian Portuguese system locales to Brazilian Portuguese", () => {
|
it("does not map non-Brazilian Portuguese system locales to Brazilian Portuguese", () => {
|
||||||
expect(resolveSupportedLocale("system", ["pt-PT"])).toBe("en");
|
expect(resolveSupportedLocale("system", ["pt-PT"])).toBe("en");
|
||||||
expect(resolveSupportedLocale("system", ["pt-AO"])).toBe("en");
|
expect(resolveSupportedLocale("system", ["pt-AO"])).toBe("en");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type SupportedLocale = "ar" | "en" | "es" | "fr" | "ja" | "pt-BR" | "ru" | "zh-CN";
|
export type SupportedLocale = "ar" | "en" | "es" | "fr" | "ja" | "ko" | "pt-BR" | "ru" | "zh-CN";
|
||||||
export type AppLanguage = "system" | SupportedLocale;
|
export type AppLanguage = "system" | SupportedLocale;
|
||||||
|
|
||||||
export interface LanguageOption {
|
export interface LanguageOption {
|
||||||
@@ -15,6 +15,7 @@ export const LANGUAGE_OPTIONS: LanguageOption[] = [
|
|||||||
{ value: "es", labelKey: "settings.general.language.options.es" },
|
{ value: "es", labelKey: "settings.general.language.options.es" },
|
||||||
{ value: "fr", labelKey: "settings.general.language.options.fr" },
|
{ value: "fr", labelKey: "settings.general.language.options.fr" },
|
||||||
{ value: "ja", labelKey: "settings.general.language.options.ja" },
|
{ value: "ja", labelKey: "settings.general.language.options.ja" },
|
||||||
|
{ value: "ko", labelKey: "settings.general.language.options.ko" },
|
||||||
{ value: "pt-BR", labelKey: "settings.general.language.options.ptBR" },
|
{ value: "pt-BR", labelKey: "settings.general.language.options.ptBR" },
|
||||||
{ value: "ru", labelKey: "settings.general.language.options.ru" },
|
{ value: "ru", labelKey: "settings.general.language.options.ru" },
|
||||||
{ value: "zh-CN", labelKey: "settings.general.language.options.zhCN" },
|
{ value: "zh-CN", labelKey: "settings.general.language.options.zhCN" },
|
||||||
@@ -27,6 +28,7 @@ const SUPPORTED_LANGUAGES = new Set<AppLanguage>([
|
|||||||
"es",
|
"es",
|
||||||
"fr",
|
"fr",
|
||||||
"ja",
|
"ja",
|
||||||
|
"ko",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
"ru",
|
"ru",
|
||||||
"zh-CN",
|
"zh-CN",
|
||||||
@@ -37,6 +39,7 @@ const LANGUAGE_NATIVE_NAMES: Record<SupportedLocale, string> = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
"pt-BR": "Português brasileiro",
|
"pt-BR": "Português brasileiro",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
"zh-CN": "简体中文",
|
"zh-CN": "简体中文",
|
||||||
@@ -48,6 +51,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "الإسبانية",
|
es: "الإسبانية",
|
||||||
fr: "الفرنسية",
|
fr: "الفرنسية",
|
||||||
ja: "اليابانية",
|
ja: "اليابانية",
|
||||||
|
ko: "الكورية",
|
||||||
"pt-BR": "البرتغالية البرازيلية",
|
"pt-BR": "البرتغالية البرازيلية",
|
||||||
ru: "الروسية",
|
ru: "الروسية",
|
||||||
"zh-CN": "الصينية المبسطة",
|
"zh-CN": "الصينية المبسطة",
|
||||||
@@ -58,6 +62,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "Spanish",
|
es: "Spanish",
|
||||||
fr: "French",
|
fr: "French",
|
||||||
ja: "Japanese",
|
ja: "Japanese",
|
||||||
|
ko: "Korean",
|
||||||
"pt-BR": "Brazilian Portuguese",
|
"pt-BR": "Brazilian Portuguese",
|
||||||
ru: "Russian",
|
ru: "Russian",
|
||||||
"zh-CN": "Simplified Chinese",
|
"zh-CN": "Simplified Chinese",
|
||||||
@@ -68,6 +73,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "español",
|
es: "español",
|
||||||
fr: "francés",
|
fr: "francés",
|
||||||
ja: "japonés",
|
ja: "japonés",
|
||||||
|
ko: "coreano",
|
||||||
"pt-BR": "portugués brasileño",
|
"pt-BR": "portugués brasileño",
|
||||||
ru: "ruso",
|
ru: "ruso",
|
||||||
"zh-CN": "chino simplificado",
|
"zh-CN": "chino simplificado",
|
||||||
@@ -78,6 +84,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "espagnol",
|
es: "espagnol",
|
||||||
fr: "français",
|
fr: "français",
|
||||||
ja: "japonais",
|
ja: "japonais",
|
||||||
|
ko: "coréen",
|
||||||
"pt-BR": "portugais brésilien",
|
"pt-BR": "portugais brésilien",
|
||||||
ru: "russe",
|
ru: "russe",
|
||||||
"zh-CN": "chinois simplifié",
|
"zh-CN": "chinois simplifié",
|
||||||
@@ -88,16 +95,29 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "スペイン語",
|
es: "スペイン語",
|
||||||
fr: "フランス語",
|
fr: "フランス語",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "韓国語",
|
||||||
"pt-BR": "ブラジルポルトガル語",
|
"pt-BR": "ブラジルポルトガル語",
|
||||||
ru: "ロシア語",
|
ru: "ロシア語",
|
||||||
"zh-CN": "簡体字中国語",
|
"zh-CN": "簡体字中国語",
|
||||||
},
|
},
|
||||||
|
ko: {
|
||||||
|
ar: "아랍어",
|
||||||
|
en: "영어",
|
||||||
|
es: "스페인어",
|
||||||
|
fr: "프랑스어",
|
||||||
|
ja: "일본어",
|
||||||
|
ko: "한국어",
|
||||||
|
"pt-BR": "브라질 포르투갈어",
|
||||||
|
ru: "러시아어",
|
||||||
|
"zh-CN": "중국어 간체",
|
||||||
|
},
|
||||||
"pt-BR": {
|
"pt-BR": {
|
||||||
ar: "árabe",
|
ar: "árabe",
|
||||||
en: "inglês",
|
en: "inglês",
|
||||||
es: "espanhol",
|
es: "espanhol",
|
||||||
fr: "francês",
|
fr: "francês",
|
||||||
ja: "japonês",
|
ja: "japonês",
|
||||||
|
ko: "coreano",
|
||||||
"pt-BR": "Português brasileiro",
|
"pt-BR": "Português brasileiro",
|
||||||
ru: "russo",
|
ru: "russo",
|
||||||
"zh-CN": "chinês simplificado",
|
"zh-CN": "chinês simplificado",
|
||||||
@@ -108,6 +128,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "испанский",
|
es: "испанский",
|
||||||
fr: "французский",
|
fr: "французский",
|
||||||
ja: "японский",
|
ja: "японский",
|
||||||
|
ko: "корейский",
|
||||||
"pt-BR": "бразильский португальский",
|
"pt-BR": "бразильский португальский",
|
||||||
ru: "русский",
|
ru: "русский",
|
||||||
"zh-CN": "упрощенный китайский",
|
"zh-CN": "упрощенный китайский",
|
||||||
@@ -118,12 +139,23 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
|
|||||||
es: "西班牙语",
|
es: "西班牙语",
|
||||||
fr: "法语",
|
fr: "法语",
|
||||||
ja: "日语",
|
ja: "日语",
|
||||||
|
ko: "韩语",
|
||||||
"pt-BR": "巴西葡萄牙语",
|
"pt-BR": "巴西葡萄牙语",
|
||||||
ru: "俄语",
|
ru: "俄语",
|
||||||
"zh-CN": "简体中文",
|
"zh-CN": "简体中文",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const REGIONAL_LANGUAGE_LOCALES: Readonly<Record<string, SupportedLocale>> = {
|
||||||
|
ar: "ar",
|
||||||
|
en: "en",
|
||||||
|
es: "es",
|
||||||
|
fr: "fr",
|
||||||
|
ja: "ja",
|
||||||
|
ko: "ko",
|
||||||
|
ru: "ru",
|
||||||
|
};
|
||||||
|
|
||||||
export function parseAppLanguage(value: unknown): AppLanguage | null {
|
export function parseAppLanguage(value: unknown): AppLanguage | null {
|
||||||
return typeof value === "string" && SUPPORTED_LANGUAGES.has(value as AppLanguage)
|
return typeof value === "string" && SUPPORTED_LANGUAGES.has(value as AppLanguage)
|
||||||
? (value as AppLanguage)
|
? (value as AppLanguage)
|
||||||
@@ -158,27 +190,14 @@ export function resolveSupportedLocale(
|
|||||||
|
|
||||||
for (const locale of systemLocales) {
|
for (const locale of systemLocales) {
|
||||||
const normalized = locale.toLowerCase();
|
const normalized = locale.toLowerCase();
|
||||||
if (normalized === "ar" || normalized.startsWith("ar-")) {
|
const baseLanguage = normalized.split("-", 1)[0];
|
||||||
return "ar";
|
const regionalLocale = REGIONAL_LANGUAGE_LOCALES[baseLanguage];
|
||||||
}
|
if (regionalLocale) {
|
||||||
if (normalized === "en" || normalized.startsWith("en-")) {
|
return regionalLocale;
|
||||||
return "en";
|
|
||||||
}
|
|
||||||
if (normalized === "es" || normalized.startsWith("es-")) {
|
|
||||||
return "es";
|
|
||||||
}
|
|
||||||
if (normalized === "fr" || normalized.startsWith("fr-")) {
|
|
||||||
return "fr";
|
|
||||||
}
|
|
||||||
if (normalized === "ja" || normalized.startsWith("ja-")) {
|
|
||||||
return "ja";
|
|
||||||
}
|
}
|
||||||
if (normalized === "pt" || normalized === "pt-br") {
|
if (normalized === "pt" || normalized === "pt-br") {
|
||||||
return "pt-BR";
|
return "pt-BR";
|
||||||
}
|
}
|
||||||
if (normalized === "ru" || normalized.startsWith("ru-")) {
|
|
||||||
return "ru";
|
|
||||||
}
|
|
||||||
if (normalized === "zh" || normalized === "zh-cn" || normalized.startsWith("zh-hans")) {
|
if (normalized === "zh" || normalized === "zh-cn" || normalized.startsWith("zh-hans")) {
|
||||||
return "zh-CN";
|
return "zh-CN";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { en } from "./resources/en";
|
|||||||
import { es } from "./resources/es";
|
import { es } from "./resources/es";
|
||||||
import { fr } from "./resources/fr";
|
import { fr } from "./resources/fr";
|
||||||
import { ja } from "./resources/ja";
|
import { ja } from "./resources/ja";
|
||||||
|
import { ko } from "./resources/ko";
|
||||||
import { ptBR } from "./resources/pt-BR";
|
import { ptBR } from "./resources/pt-BR";
|
||||||
import { ru } from "./resources/ru";
|
import { ru } from "./resources/ru";
|
||||||
import { zhCN } from "./resources/zh-CN";
|
import { zhCN } from "./resources/zh-CN";
|
||||||
@@ -109,6 +110,7 @@ describe("translation resources", () => {
|
|||||||
expect(flattenKeys(es).sort()).toEqual(englishKeys);
|
expect(flattenKeys(es).sort()).toEqual(englishKeys);
|
||||||
expect(flattenKeys(fr).sort()).toEqual(englishKeys);
|
expect(flattenKeys(fr).sort()).toEqual(englishKeys);
|
||||||
expect(flattenKeys(ja).sort()).toEqual(englishKeys);
|
expect(flattenKeys(ja).sort()).toEqual(englishKeys);
|
||||||
|
expect(flattenKeys(ko).sort()).toEqual(englishKeys);
|
||||||
expect(flattenKeys(ptBR).sort()).toEqual(englishKeys);
|
expect(flattenKeys(ptBR).sort()).toEqual(englishKeys);
|
||||||
expect(flattenKeys(ru).sort()).toEqual(englishKeys);
|
expect(flattenKeys(ru).sort()).toEqual(englishKeys);
|
||||||
expect(flattenKeys(zhCN).sort()).toEqual(englishKeys);
|
expect(flattenKeys(zhCN).sort()).toEqual(englishKeys);
|
||||||
@@ -121,6 +123,7 @@ describe("translation resources", () => {
|
|||||||
expect(countMatchingEnglishStrings(es)).toBeLessThan(maxFallbackStrings);
|
expect(countMatchingEnglishStrings(es)).toBeLessThan(maxFallbackStrings);
|
||||||
expect(countMatchingEnglishStrings(fr)).toBeLessThan(maxFallbackStrings);
|
expect(countMatchingEnglishStrings(fr)).toBeLessThan(maxFallbackStrings);
|
||||||
expect(countMatchingEnglishStrings(ja)).toBeLessThan(maxFallbackStrings);
|
expect(countMatchingEnglishStrings(ja)).toBeLessThan(maxFallbackStrings);
|
||||||
|
expect(countMatchingEnglishStrings(ko)).toBeLessThan(maxFallbackStrings);
|
||||||
expect(countMatchingEnglishStrings(ptBR)).toBeLessThan(maxFallbackStrings);
|
expect(countMatchingEnglishStrings(ptBR)).toBeLessThan(maxFallbackStrings);
|
||||||
expect(countMatchingEnglishStrings(ru)).toBeLessThan(maxFallbackStrings);
|
expect(countMatchingEnglishStrings(ru)).toBeLessThan(maxFallbackStrings);
|
||||||
expect(countMatchingEnglishStrings(zhCN)).toBeLessThan(maxFallbackStrings);
|
expect(countMatchingEnglishStrings(zhCN)).toBeLessThan(maxFallbackStrings);
|
||||||
@@ -131,6 +134,7 @@ describe("translation resources", () => {
|
|||||||
expect(findInterpolationMismatches(es)).toEqual([]);
|
expect(findInterpolationMismatches(es)).toEqual([]);
|
||||||
expect(findInterpolationMismatches(fr)).toEqual([]);
|
expect(findInterpolationMismatches(fr)).toEqual([]);
|
||||||
expect(findInterpolationMismatches(ja)).toEqual([]);
|
expect(findInterpolationMismatches(ja)).toEqual([]);
|
||||||
|
expect(findInterpolationMismatches(ko)).toEqual([]);
|
||||||
expect(findInterpolationMismatches(ptBR)).toEqual([]);
|
expect(findInterpolationMismatches(ptBR)).toEqual([]);
|
||||||
expect(findInterpolationMismatches(ru)).toEqual([]);
|
expect(findInterpolationMismatches(ru)).toEqual([]);
|
||||||
expect(findInterpolationMismatches(zhCN)).toEqual([]);
|
expect(findInterpolationMismatches(zhCN)).toEqual([]);
|
||||||
@@ -147,6 +151,7 @@ describe("translation resources", () => {
|
|||||||
expect(es.modelSelector.modelCountPlural).toBe("{{count}} modelos");
|
expect(es.modelSelector.modelCountPlural).toBe("{{count}} modelos");
|
||||||
expect(fr.modelSelector.modelCountPlural).toBe("{{count}} modèles");
|
expect(fr.modelSelector.modelCountPlural).toBe("{{count}} modèles");
|
||||||
expect(ja.modelSelector.modelCountPlural).toBe("{{count}}つのモデル");
|
expect(ja.modelSelector.modelCountPlural).toBe("{{count}}つのモデル");
|
||||||
|
expect(ko.modelSelector.modelCountPlural).toBe("모델 {{count}}개");
|
||||||
expect(ptBR.modelSelector.modelCountPlural).toBe("{{count}} modelos");
|
expect(ptBR.modelSelector.modelCountPlural).toBe("{{count}} modelos");
|
||||||
expect(ru.modelSelector.modelCountPlural).toBe("{{count}} моделей");
|
expect(ru.modelSelector.modelCountPlural).toBe("{{count}} моделей");
|
||||||
expect(zhCN.modelSelector.modelCountPlural).toBe("{{count}} 个模型");
|
expect(zhCN.modelSelector.modelCountPlural).toBe("{{count}} 个模型");
|
||||||
@@ -159,6 +164,11 @@ describe("translation resources", () => {
|
|||||||
expect(zhCN.settings.providers.models.many).toBe("{{count}} 个 Model");
|
expect(zhCN.settings.providers.models.many).toBe("{{count}} 个 Model");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves reviewed Korean status labels", () => {
|
||||||
|
expect(ko.common.states.starting).toBe("시작 중...");
|
||||||
|
expect(ko.desktop.daemon.status.notRunning).toBe("실행 중이 아님");
|
||||||
|
});
|
||||||
|
|
||||||
it("labels the immediate add-to-chat action without an ellipsis", () => {
|
it("labels the immediate add-to-chat action without an ellipsis", () => {
|
||||||
expect(en.workspace.fileActions.addToChat).toBe("Add to chat");
|
expect(en.workspace.fileActions.addToChat).toBe("Add to chat");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1750,6 +1750,7 @@ export const ar: TranslationResources = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
ptBR: "Português brasileiro",
|
ptBR: "Português brasileiro",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
zhCN: "中文",
|
zhCN: "中文",
|
||||||
|
|||||||
@@ -1760,6 +1760,7 @@ export const en = {
|
|||||||
es: "Spanish",
|
es: "Spanish",
|
||||||
fr: "French",
|
fr: "French",
|
||||||
ja: "Japanese",
|
ja: "Japanese",
|
||||||
|
ko: "Korean",
|
||||||
ptBR: "Brazilian Portuguese",
|
ptBR: "Brazilian Portuguese",
|
||||||
ru: "Russian",
|
ru: "Russian",
|
||||||
zhCN: "Simplified Chinese",
|
zhCN: "Simplified Chinese",
|
||||||
|
|||||||
@@ -1797,6 +1797,7 @@ export const es: TranslationResources = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
ptBR: "Português brasileiro",
|
ptBR: "Português brasileiro",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
zhCN: "中文",
|
zhCN: "中文",
|
||||||
|
|||||||
@@ -1799,6 +1799,7 @@ export const fr: TranslationResources = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
ptBR: "Português brasileiro",
|
ptBR: "Português brasileiro",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
zhCN: "中文",
|
zhCN: "中文",
|
||||||
|
|||||||
@@ -1765,6 +1765,7 @@ export const ja: TranslationResources = {
|
|||||||
es: "スペイン語",
|
es: "スペイン語",
|
||||||
fr: "フランス語",
|
fr: "フランス語",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "韓国語",
|
||||||
ptBR: "ブラジルポルトガル語",
|
ptBR: "ブラジルポルトガル語",
|
||||||
ru: "ロシア語",
|
ru: "ロシア語",
|
||||||
zhCN: "簡体字中国語",
|
zhCN: "簡体字中国語",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1781,6 +1781,7 @@ export const ptBR: TranslationResources = {
|
|||||||
es: "Espanhol",
|
es: "Espanhol",
|
||||||
fr: "Francês",
|
fr: "Francês",
|
||||||
ja: "Japonês",
|
ja: "Japonês",
|
||||||
|
ko: "Coreano",
|
||||||
ptBR: "Português brasileiro",
|
ptBR: "Português brasileiro",
|
||||||
ru: "Russo",
|
ru: "Russo",
|
||||||
zhCN: "Chinês simplificado",
|
zhCN: "Chinês simplificado",
|
||||||
|
|||||||
@@ -1785,6 +1785,7 @@ export const ru: TranslationResources = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
ptBR: "Português brasileiro",
|
ptBR: "Português brasileiro",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
zhCN: "中文",
|
zhCN: "中文",
|
||||||
|
|||||||
@@ -1729,6 +1729,7 @@ export const zhCN: TranslationResources = {
|
|||||||
es: "Español",
|
es: "Español",
|
||||||
fr: "Français",
|
fr: "Français",
|
||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
|
ko: "한국어",
|
||||||
ptBR: "Português brasileiro",
|
ptBR: "Português brasileiro",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
zhCN: "简体中文",
|
zhCN: "简体中文",
|
||||||
|
|||||||
Reference in New Issue
Block a user