feat(i18n): add Japanese (ja) locale (#1694)

* 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>
This commit is contained in:
sysCat64
2026-06-24 13:11:35 +00:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 70ea960153
commit 507345dbee
14 changed files with 2139 additions and 10 deletions
+2
View File
@@ -5,6 +5,7 @@ import { ar } from "./resources/ar";
import { en } from "./resources/en";
import { es } from "./resources/es";
import { fr } from "./resources/fr";
import { ja } from "./resources/ja";
import { ru } from "./resources/ru";
import { zhCN } from "./resources/zh-CN";
@@ -20,6 +21,7 @@ observeI18nInit(
en: { translation: en },
es: { translation: es },
fr: { translation: fr },
ja: { translation: ja },
ru: { translation: ru },
"zh-CN": { translation: zhCN },
},
+8 -4
View File
@@ -7,13 +7,14 @@ import {
} from "./locales";
describe("parseAppLanguage", () => {
it("accepts system and all UN official language locales", () => {
expect(["system", "ar", "en", "es", "fr", "ru", "zh-CN"].map(parseAppLanguage)).toEqual([
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",
]);
@@ -24,13 +25,14 @@ describe("parseAppLanguage", () => {
expect(parseAppLanguage(null)).toBeNull();
});
it("offers system plus the six UN official languages", () => {
it("offers system plus all supported languages", () => {
expect(LANGUAGE_OPTIONS.map((option) => option.value)).toEqual([
"system",
"ar",
"en",
"es",
"fr",
"ja",
"ru",
"zh-CN",
]);
@@ -81,15 +83,17 @@ describe("resolveSupportedLocale", () => {
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 UN official system locales", () => {
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");
});
+31 -2
View File
@@ -1,4 +1,4 @@
export type SupportedLocale = "ar" | "en" | "es" | "fr" | "ru" | "zh-CN";
export type SupportedLocale = "ar" | "en" | "es" | "fr" | "ja" | "ru" | "zh-CN";
export type AppLanguage = "system" | SupportedLocale;
export interface LanguageOption {
@@ -14,16 +14,27 @@ export const LANGUAGE_OPTIONS: LanguageOption[] = [
{ value: "en", labelKey: "settings.general.language.options.en" },
{ value: "es", labelKey: "settings.general.language.options.es" },
{ value: "fr", labelKey: "settings.general.language.options.fr" },
{ value: "ja", labelKey: "settings.general.language.options.ja" },
{ value: "ru", labelKey: "settings.general.language.options.ru" },
{ value: "zh-CN", labelKey: "settings.general.language.options.zhCN" },
];
const SUPPORTED_LANGUAGES = new Set<AppLanguage>(["system", "ar", "en", "es", "fr", "ru", "zh-CN"]);
const SUPPORTED_LANGUAGES = new Set<AppLanguage>([
"system",
"ar",
"en",
"es",
"fr",
"ja",
"ru",
"zh-CN",
]);
const LANGUAGE_NATIVE_NAMES: Record<SupportedLocale, string> = {
ar: "العربية",
en: "English",
es: "Español",
fr: "Français",
ja: "日本語",
ru: "Русский",
"zh-CN": "简体中文",
};
@@ -33,6 +44,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
en: "الإنجليزية",
es: "الإسبانية",
fr: "الفرنسية",
ja: "اليابانية",
ru: "الروسية",
"zh-CN": "الصينية المبسطة",
},
@@ -41,6 +53,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
en: "English",
es: "Spanish",
fr: "French",
ja: "Japanese",
ru: "Russian",
"zh-CN": "Simplified Chinese",
},
@@ -49,6 +62,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
en: "inglés",
es: "español",
fr: "francés",
ja: "japonés",
ru: "ruso",
"zh-CN": "chino simplificado",
},
@@ -57,14 +71,25 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
en: "anglais",
es: "espagnol",
fr: "français",
ja: "japonais",
ru: "russe",
"zh-CN": "chinois simplifié",
},
ja: {
ar: "アラビア語",
en: "英語",
es: "スペイン語",
fr: "フランス語",
ja: "日本語",
ru: "ロシア語",
"zh-CN": "簡体字中国語",
},
ru: {
ar: "арабский",
en: "английский",
es: "испанский",
fr: "французский",
ja: "японский",
ru: "русский",
"zh-CN": "упрощенный китайский",
},
@@ -73,6 +98,7 @@ const LANGUAGE_NAMES_BY_LOCALE: Record<SupportedLocale, Record<SupportedLocale,
en: "英语",
es: "西班牙语",
fr: "法语",
ja: "日语",
ru: "俄语",
"zh-CN": "简体中文",
},
@@ -124,6 +150,9 @@ export function resolveSupportedLocale(
if (normalized === "fr" || normalized.startsWith("fr-")) {
return "fr";
}
if (normalized === "ja" || normalized.startsWith("ja-")) {
return "ja";
}
if (normalized === "ru" || normalized.startsWith("ru-")) {
return "ru";
}
+8 -2
View File
@@ -5,6 +5,7 @@ import { ar } from "./resources/ar";
import { en } from "./resources/en";
import { es } from "./resources/es";
import { fr } from "./resources/fr";
import { ja } from "./resources/ja";
import { ru } from "./resources/ru";
import { zhCN } from "./resources/zh-CN";
@@ -101,21 +102,23 @@ function findUntranslatedConnectionErrors(): string[] {
}
describe("translation resources", () => {
it("keeps UN official language keys in sync with English", () => {
it("keeps all supported language keys in sync with English", () => {
const englishKeys = flattenKeys(en).sort();
expect(flattenKeys(ar).sort()).toEqual(englishKeys);
expect(flattenKeys(es).sort()).toEqual(englishKeys);
expect(flattenKeys(fr).sort()).toEqual(englishKeys);
expect(flattenKeys(ja).sort()).toEqual(englishKeys);
expect(flattenKeys(ru).sort()).toEqual(englishKeys);
expect(flattenKeys(zhCN).sort()).toEqual(englishKeys);
});
it("keeps non-English UN official languages translated beyond fallback labels", () => {
it("keeps non-English supported languages translated beyond fallback labels", () => {
const totalStrings = Object.keys(flattenStrings(en)).length;
const maxFallbackStrings = Math.floor(totalStrings * 0.25);
expect(countMatchingEnglishStrings(ar)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(es)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(fr)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(ja)).toBeLessThan(maxFallbackStrings);
expect(countMatchingEnglishStrings(ru)).toBeLessThan(maxFallbackStrings);
});
@@ -123,6 +126,7 @@ describe("translation resources", () => {
expect(findInterpolationMismatches(ar)).toEqual([]);
expect(findInterpolationMismatches(es)).toEqual([]);
expect(findInterpolationMismatches(fr)).toEqual([]);
expect(findInterpolationMismatches(ja)).toEqual([]);
expect(findInterpolationMismatches(ru)).toEqual([]);
expect(findInterpolationMismatches(zhCN)).toEqual([]);
});
@@ -137,11 +141,13 @@ describe("translation resources", () => {
expect(ar.modelSelector.modelCountPlural).toBe("{{count}} نماذج");
expect(es.modelSelector.modelCountPlural).toBe("{{count}} modelos");
expect(fr.modelSelector.modelCountPlural).toBe("{{count}} modèles");
expect(ja.modelSelector.modelCountPlural).toBe("{{count}}つのモデル");
expect(ru.modelSelector.modelCountPlural).toBe("{{count}} моделей");
expect(zhCN.modelSelector.modelCountPlural).toBe("{{count}} 个模型");
expect(ar.settings.providers.models.many).toBe("{{count}} نماذج");
expect(es.settings.providers.models.many).toBe("{{count}} modelos");
expect(fr.settings.providers.models.many).toBe("{{count}} modèles");
expect(ja.settings.providers.models.many).toBe("{{count}}つのモデル");
expect(ru.settings.providers.models.many).toBe("{{count}} моделей");
expect(zhCN.settings.providers.models.many).toBe("{{count}} 个 Model");
});
+1
View File
@@ -1438,6 +1438,7 @@ export const ar: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
+1
View File
@@ -1444,6 +1444,7 @@ export const en = {
en: "English",
es: "Spanish",
fr: "French",
ja: "Japanese",
ru: "Russian",
zhCN: "Simplified Chinese",
},
+1
View File
@@ -1475,6 +1475,7 @@ export const es: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
+1
View File
@@ -1478,6 +1478,7 @@ export const fr: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1465,6 +1465,7 @@ export const ru: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
ja: "日本語",
ru: "Русский",
zhCN: "中文",
},
+1
View File
@@ -1420,6 +1420,7 @@ export const zhCN: TranslationResources = {
en: "English",
es: "Español",
fr: "Français",
ja: "日本語",
ru: "Русский",
zhCN: "简体中文",
},