diff --git a/packages/kilo-vscode/tests/unit/language-utils.test.ts b/packages/kilo-vscode/tests/unit/language-utils.test.ts new file mode 100644 index 00000000000..75ad9bd5f8c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/language-utils.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from "bun:test" +import { normalizeLocale, resolveTemplate } from "../../webview-ui/src/context/language-utils" + +describe("normalizeLocale", () => { + it("returns 'en' for English", () => { + expect(normalizeLocale("en")).toBe("en") + expect(normalizeLocale("en-US")).toBe("en") + expect(normalizeLocale("en-GB")).toBe("en") + }) + + it("returns 'zh' for Simplified Chinese", () => { + expect(normalizeLocale("zh")).toBe("zh") + expect(normalizeLocale("zh-CN")).toBe("zh") + expect(normalizeLocale("zh-Hans")).toBe("zh") + }) + + it("returns 'zht' for Traditional Chinese", () => { + expect(normalizeLocale("zh-Hant")).toBe("zht") + expect(normalizeLocale("zh-TW")).toBe("zh") + expect(normalizeLocale("zh-hant-TW")).toBe("zht") + }) + + it("returns 'de' for German", () => { + expect(normalizeLocale("de")).toBe("de") + expect(normalizeLocale("de-AT")).toBe("de") + }) + + it("returns 'ko' for Korean", () => { + expect(normalizeLocale("ko")).toBe("ko") + expect(normalizeLocale("ko-KR")).toBe("ko") + }) + + it("returns 'no' for Norwegian Bokmål", () => { + expect(normalizeLocale("nb")).toBe("no") + expect(normalizeLocale("nb-NO")).toBe("no") + }) + + it("returns 'no' for Norwegian Nynorsk", () => { + expect(normalizeLocale("nn")).toBe("no") + }) + + it("returns 'br' for Portuguese", () => { + expect(normalizeLocale("pt")).toBe("br") + expect(normalizeLocale("pt-BR")).toBe("br") + expect(normalizeLocale("pt-PT")).toBe("br") + }) + + it("falls back to 'en' for unknown locale", () => { + expect(normalizeLocale("xx")).toBe("en") + expect(normalizeLocale("xyz-ZZ")).toBe("en") + }) + + it("is case-insensitive", () => { + expect(normalizeLocale("EN")).toBe("en") + expect(normalizeLocale("DE")).toBe("de") + expect(normalizeLocale("ZH-HANT")).toBe("zht") + }) +}) + +describe("resolveTemplate", () => { + it("returns text unchanged when no params", () => { + expect(resolveTemplate("hello world")).toBe("hello world") + }) + + it("returns text unchanged when params is undefined", () => { + expect(resolveTemplate("no {{var}} here", undefined)).toBe("no {{var}} here") + }) + + it("interpolates a single variable", () => { + expect(resolveTemplate("Hello {{name}}!", { name: "World" })).toBe("Hello World!") + }) + + it("interpolates multiple variables", () => { + const result = resolveTemplate("{{a}} + {{b}} = {{c}}", { a: "1", b: "2", c: "3" }) + expect(result).toBe("1 + 2 = 3") + }) + + it("replaces missing variable with empty string", () => { + expect(resolveTemplate("{{missing}}", {})).toBe("") + }) + + it("handles numeric variable values", () => { + expect(resolveTemplate("count: {{n}}", { n: 42 })).toBe("count: 42") + }) + + it("handles whitespace around key in braces", () => { + expect(resolveTemplate("{{ name }}", { name: "test" })).toBe("test") + }) + + it("leaves unrelated text intact", () => { + const result = resolveTemplate("prefix {{x}} suffix", { x: "VALUE" }) + expect(result).toBe("prefix VALUE suffix") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/context/language-utils.ts b/packages/kilo-vscode/webview-ui/src/context/language-utils.ts new file mode 100644 index 00000000000..46b6b04f792 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/language-utils.ts @@ -0,0 +1,70 @@ +export type Locale = + | "en" + | "zh" + | "zht" + | "ko" + | "de" + | "es" + | "fr" + | "da" + | "ja" + | "pl" + | "ru" + | "ar" + | "no" + | "br" + | "th" + | "bs" + +export const LOCALES: readonly Locale[] = [ + "en", + "zh", + "zht", + "ko", + "de", + "es", + "fr", + "da", + "ja", + "pl", + "ru", + "ar", + "no", + "br", + "th", + "bs", +] + +/** + * Normalize a BCP 47 language tag to one of the supported Locale values. + * Falls back to "en" for unrecognized locales. + */ +export function normalizeLocale(lang: string): Locale { + const lower = lang.toLowerCase() + if (lower.startsWith("zh")) { + return lower.includes("hant") ? "zht" : "zh" + } + for (const loc of LOCALES) { + if (lower.startsWith(loc)) { + return loc + } + } + if (lower.startsWith("nb") || lower.startsWith("nn")) { + return "no" + } + if (lower.startsWith("pt")) { + return "br" + } + return "en" +} + +/** + * Perform {{key}} template interpolation against a params record. + */ +export function resolveTemplate(text: string, params?: Record): string { + if (!params) return text + return text.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_, rawKey) => { + const value = params[String(rawKey)] + return value === undefined ? "" : String(value) + }) +} diff --git a/packages/kilo-vscode/webview-ui/src/context/language.tsx b/packages/kilo-vscode/webview-ui/src/context/language.tsx index ae2bb645535..2fd2d631222 100644 --- a/packages/kilo-vscode/webview-ui/src/context/language.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/language.tsx @@ -58,43 +58,12 @@ import { dict as kiloBr } from "@kilocode/kilo-i18n/br" import { dict as kiloTh } from "@kilocode/kilo-i18n/th" import { dict as kiloBs } from "@kilocode/kilo-i18n/bs" import { useVSCode } from "./vscode" +import { normalizeLocale as _normalizeLocale, resolveTemplate as _resolveTemplate } from "./language-utils" -export type Locale = - | "en" - | "zh" - | "zht" - | "ko" - | "de" - | "es" - | "fr" - | "da" - | "ja" - | "pl" - | "ru" - | "ar" - | "no" - | "br" - | "th" - | "bs" - -export const LOCALES: readonly Locale[] = [ - "en", - "zh", - "zht", - "ko", - "de", - "es", - "fr", - "da", - "ja", - "pl", - "ru", - "ar", - "no", - "br", - "th", - "bs", -] +export type { Locale } from "./language-utils" +export { LOCALES } from "./language-utils" +import type { Locale } from "./language-utils" +import { LOCALES } from "./language-utils" export const LOCALE_LABELS: Record = { en: "English", @@ -137,33 +106,11 @@ const dicts: Record> = { } function normalizeLocale(lang: string): Locale { - const lower = lang.toLowerCase() - if (lower.startsWith("zh")) { - return lower.includes("hant") ? "zht" : "zh" - } - for (const loc of LOCALES) { - if (lower.startsWith(loc)) { - return loc - } - } - // Special cases - if (lower.startsWith("nb") || lower.startsWith("nn")) { - return "no" - } - if (lower.startsWith("pt")) { - return "br" - } - return "en" + return _normalizeLocale(lang) } function resolveTemplate(text: string, params?: UiI18nParams) { - if (!params) { - return text - } - return text.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_, rawKey) => { - const value = params[String(rawKey)] - return value === undefined ? "" : String(value) - }) + return _resolveTemplate(text, params as Record) } interface LanguageProviderProps {