feat: add multi-language support for 20 locales

Add complete i18n infrastructure with 21 supported languages:
English, Simplified Chinese, Traditional Chinese, Japanese, Korean,
Spanish, French, Italian, Brazilian Portuguese, German, Dutch, Swedish,
Russian, Polish, Ukrainian, Arabic (RTL), Turkish, Hindi, Vietnamese,
Indonesian, and Thai.

- I18nProvider context with three-tier locale detection
  (user preference > navigator.languages > instance default > English)
- ~1500 translation keys per locale with TypeScript-enforced completeness
- Dynamic code-splitting: only the active locale is loaded at runtime
- Language selectors in footer, login page, settings, and mobile sidebar
- Arabic RTL support with CSS logical properties across all components
- Tool names, descriptions, and categories translated via i18n helpers
- Public API endpoint GET /api/v1/config/locale for instance default
- Multi-script font stack (CJK, Arabic, Devanagari, Thai, Cyrillic)
- format() and plural() helpers for interpolation and pluralization
- API error translation mapping (translateApiError)
- 36 Playwright e2e tests verifying all 21 locales load correctly
- 25 unit tests for format, plural, locale detection, and completeness
- Updated translations.md docs and CLAUDE.md conventions
This commit is contained in:
SnapOtter
2026-05-15 17:02:49 +08:00
parent 3a82936d93
commit d38621d7b9
141 changed files with 43160 additions and 936 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44 -1
View File
@@ -1,2 +1,45 @@
import { en } from "./en.js";
export type { TranslationKeys } from "./en.js";
export { en } from "./en.js";
export { en };
export type SupportedLocale = {
code: string;
name: string;
nativeName: string;
dir: "ltr" | "rtl";
};
export const SUPPORTED_LOCALES: SupportedLocale[] = [
{ code: "en", name: "English", nativeName: "English", dir: "ltr" },
{ code: "zh-CN", name: "Chinese (Simplified)", nativeName: "简体中文", dir: "ltr" },
{ code: "zh-TW", name: "Chinese (Traditional)", nativeName: "繁體中文", dir: "ltr" },
{ code: "ja", name: "Japanese", nativeName: "日本語", dir: "ltr" },
{ code: "ko", name: "Korean", nativeName: "한국어", dir: "ltr" },
{ code: "es", name: "Spanish", nativeName: "Español", dir: "ltr" },
{ code: "fr", name: "French", nativeName: "Français", dir: "ltr" },
{ code: "it", name: "Italian", nativeName: "Italiano", dir: "ltr" },
{ code: "pt-BR", name: "Portuguese (Brazil)", nativeName: "Português (Brasil)", dir: "ltr" },
{ code: "de", name: "German", nativeName: "Deutsch", dir: "ltr" },
{ code: "nl", name: "Dutch", nativeName: "Nederlands", dir: "ltr" },
{ code: "sv", name: "Swedish", nativeName: "Svenska", dir: "ltr" },
{ code: "ru", name: "Russian", nativeName: "Русский", dir: "ltr" },
{ code: "pl", name: "Polish", nativeName: "Polski", dir: "ltr" },
{ code: "uk", name: "Ukrainian", nativeName: "Українська", dir: "ltr" },
{ code: "ar", name: "Arabic", nativeName: "العربية", dir: "rtl" },
{ code: "tr", name: "Turkish", nativeName: "Türkçe", dir: "ltr" },
{ code: "hi", name: "Hindi", nativeName: "हिन्दी", dir: "ltr" },
{ code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt", dir: "ltr" },
{ code: "id", name: "Indonesian", nativeName: "Bahasa Indonesia", dir: "ltr" },
{ code: "th", name: "Thai", nativeName: "ไทย", dir: "ltr" },
];
export async function loadTranslations(locale: string): Promise<import("./en.js").TranslationKeys> {
if (locale === "en") return en;
try {
const mod = await import(`./${locale}.js`);
return mod[locale] ?? mod.default ?? en;
} catch {
return en;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff