* feat(i18n): add Japanese (ja) locale - Add packages/app/src/i18n/resources/ja.ts with 1287 keys matching en.ts - Register ja in i18next.ts, locales.ts (SupportedLocale, LANGUAGE_OPTIONS, SUPPORTED_LANGUAGES, LANGUAGE_NATIVE_NAMES, LANGUAGE_NAMES_BY_LOCALE, resolveSupportedLocale) - Add ja label key to all six existing locale files (ar, en, es, fr, ru, zh-CN) - Update locales.test.ts to cover ja in all relevant assertions Translation reviewed by Codex across four passes; terminology, placeholder parity, and naturalness confirmed clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(i18n): add ja to resources.test.ts and fix test descriptions - Import ja and add to key-parity, fallback-ratio, interpolation, and model-count assertions in resources.test.ts - Update test descriptions to say "all supported language(s)" instead of "UN official language(s)" (ja is not a UN official language) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Japanese README (README.ja.md) Add README.ja.md translated from the English source, reviewed by Codex across two passes. Link it in the language navigation of README.md and README.zh-CN.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: apply oxfmt formatting to locales.ts and ja.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
LANGUAGE_OPTIONS,
|
|
formatLanguageOptionLabel,
|
|
parseAppLanguage,
|
|
resolveSupportedLocale,
|
|
} from "./locales";
|
|
|
|
describe("parseAppLanguage", () => {
|
|
it("accepts system and all supported language locales", () => {
|
|
expect(["system", "ar", "en", "es", "fr", "ja", "ru", "zh-CN"].map(parseAppLanguage)).toEqual([
|
|
"system",
|
|
"ar",
|
|
"en",
|
|
"es",
|
|
"fr",
|
|
"ja",
|
|
"ru",
|
|
"zh-CN",
|
|
]);
|
|
});
|
|
|
|
it("returns null for unknown values", () => {
|
|
expect(parseAppLanguage("de")).toBeNull();
|
|
expect(parseAppLanguage(null)).toBeNull();
|
|
});
|
|
|
|
it("offers system plus all supported languages", () => {
|
|
expect(LANGUAGE_OPTIONS.map((option) => option.value)).toEqual([
|
|
"system",
|
|
"ar",
|
|
"en",
|
|
"es",
|
|
"fr",
|
|
"ja",
|
|
"ru",
|
|
"zh-CN",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("formatLanguageOptionLabel", () => {
|
|
it("shows the native language name and English name in English UI", () => {
|
|
const arabic = LANGUAGE_OPTIONS.find((option) => option.value === "ar");
|
|
const spanish = LANGUAGE_OPTIONS.find((option) => option.value === "es");
|
|
const chinese = LANGUAGE_OPTIONS.find((option) => option.value === "zh-CN");
|
|
|
|
expect([
|
|
formatLanguageOptionLabel(arabic!, "en", "System"),
|
|
formatLanguageOptionLabel(spanish!, "en", "System"),
|
|
formatLanguageOptionLabel(chinese!, "en", "System"),
|
|
]).toEqual(["العربية - Arabic", "Español - Spanish", "简体中文 - Simplified Chinese"]);
|
|
});
|
|
|
|
it("shows the native language name and Chinese name in Chinese UI", () => {
|
|
const arabic = LANGUAGE_OPTIONS.find((option) => option.value === "ar");
|
|
const english = LANGUAGE_OPTIONS.find((option) => option.value === "en");
|
|
const spanish = LANGUAGE_OPTIONS.find((option) => option.value === "es");
|
|
|
|
expect([
|
|
formatLanguageOptionLabel(arabic!, "zh-CN", "系统"),
|
|
formatLanguageOptionLabel(english!, "zh-CN", "系统"),
|
|
formatLanguageOptionLabel(spanish!, "zh-CN", "系统"),
|
|
]).toEqual(["العربية - 阿拉伯语", "English - 英语", "Español - 西班牙语"]);
|
|
});
|
|
|
|
it("uses a single label when both language names match", () => {
|
|
const english = LANGUAGE_OPTIONS.find((option) => option.value === "en");
|
|
|
|
expect(formatLanguageOptionLabel(english!, "en", "System")).toBe("English");
|
|
});
|
|
|
|
it("uses the active-language name for System", () => {
|
|
const system = LANGUAGE_OPTIONS.find((option) => option.value === "system");
|
|
|
|
expect(formatLanguageOptionLabel(system!, "zh-CN", "系统")).toBe("系统");
|
|
});
|
|
});
|
|
|
|
describe("resolveSupportedLocale", () => {
|
|
it("respects explicit language choices", () => {
|
|
expect(resolveSupportedLocale("ar", ["en-US"])).toBe("ar");
|
|
expect(resolveSupportedLocale("en", ["zh-CN"])).toBe("en");
|
|
expect(resolveSupportedLocale("es", ["en-US"])).toBe("es");
|
|
expect(resolveSupportedLocale("fr", ["en-US"])).toBe("fr");
|
|
expect(resolveSupportedLocale("ja", ["en-US"])).toBe("ja");
|
|
expect(resolveSupportedLocale("ru", ["en-US"])).toBe("ru");
|
|
expect(resolveSupportedLocale("zh-CN", ["en-US"])).toBe("zh-CN");
|
|
});
|
|
|
|
it("maps supported system locales", () => {
|
|
expect(resolveSupportedLocale("system", ["ar-EG"])).toBe("ar");
|
|
expect(resolveSupportedLocale("system", ["en-US"])).toBe("en");
|
|
expect(resolveSupportedLocale("system", ["es-MX"])).toBe("es");
|
|
expect(resolveSupportedLocale("system", ["fr-CA"])).toBe("fr");
|
|
expect(resolveSupportedLocale("system", ["ja-JP"])).toBe("ja");
|
|
expect(resolveSupportedLocale("system", ["ru-RU"])).toBe("ru");
|
|
});
|
|
|
|
it("keeps English when Spanish is a secondary system language", () => {
|
|
expect(resolveSupportedLocale("system", ["en-US", "es-GB"])).toBe("en");
|
|
});
|
|
|
|
it("maps Chinese system locales to Simplified Chinese", () => {
|
|
expect(resolveSupportedLocale("system", ["zh"])).toBe("zh-CN");
|
|
expect(resolveSupportedLocale("system", ["zh-CN"])).toBe("zh-CN");
|
|
expect(resolveSupportedLocale("system", ["zh-Hans-US"])).toBe("zh-CN");
|
|
});
|
|
|
|
it("does not map Traditional Chinese system locales to Simplified Chinese", () => {
|
|
expect(resolveSupportedLocale("system", ["zh-TW"])).toBe("en");
|
|
expect(resolveSupportedLocale("system", ["zh-Hant"])).toBe("en");
|
|
expect(resolveSupportedLocale("system", ["zh-HK"])).toBe("en");
|
|
});
|
|
|
|
it("maps unsupported or missing system locales to English", () => {
|
|
expect(resolveSupportedLocale("system", ["de-DE"])).toBe("en");
|
|
expect(resolveSupportedLocale("system", [])).toBe("en");
|
|
});
|
|
});
|