From 2a2183a3d23d2c8cbe65dc2440fc08bded541f1a Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 14 May 2026 15:41:30 -0500 Subject: [PATCH] Restore i18next i18n wrapper Co-Authored-By: Paperclip --- ui/package.json | 3 +++ ui/src/i18n/index.ts | 37 ++++++++++++++++++------------------- ui/src/i18n/locales.ts | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/ui/package.json b/ui/package.json index 65caa25ee74..a73c568ed2d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "build-storybook": "storybook build -c storybook/.storybook -o storybook-static", "preview": "vite preview", "typecheck": "tsc -b", + "test": "vitest run", "clean": "rm -rf dist storybook-static tsconfig.tsbuildinfo", "prepack": "rm -f package.dev.json && cp package.json package.dev.json && node ../scripts/generate-ui-package-json.mjs", "postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi" @@ -52,12 +53,14 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "hermes-paperclip-adapter": "^0.2.0", + "i18next": "^26.1.0", "lexical": "0.35.0", "lucide-react": "^0.574.0", "mermaid": "^11.12.0", "radix-ui": "^1.4.3", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^17.0.7", "react-markdown": "^10.1.0", "react-router-dom": "^7.1.5", "remark-gfm": "^4.0.1", diff --git a/ui/src/i18n/index.ts b/ui/src/i18n/index.ts index ee0f1974da5..2ab68d20d6b 100644 --- a/ui/src/i18n/index.ts +++ b/ui/src/i18n/index.ts @@ -1,25 +1,24 @@ -import { DEFAULT_LOCALE, localeMessages } from "./locales"; +import i18n, { type InitOptions, type TOptions } from "i18next"; +import { initReactI18next, useTranslation as useReactI18nextTranslation } from "react-i18next"; -type TranslationOptions = { - defaultValue?: string; +import { DEFAULT_LOCALE, i18nextResources, supportedLocales } from "./locales"; + +const i18nextOptions: InitOptions = { + resources: i18nextResources, + lng: DEFAULT_LOCALE, + fallbackLng: DEFAULT_LOCALE, + supportedLngs: supportedLocales, + defaultNS: "translation", + interpolation: { escapeValue: false }, + returnObjects: false, + initAsync: false, }; -function resolveLocaleValue(key: string) { - const path = key.split("."); - let value: unknown = localeMessages[DEFAULT_LOCALE]; +i18n.use(initReactI18next).init(i18nextOptions); - for (const segment of path) { - if (!value || typeof value !== "object" || Array.isArray(value)) return null; - value = (value as Record)[segment]; - } - - return typeof value === "string" ? value : null; +export function t(key: string, options: TOptions = {}) { + return i18n.t(key, options); } -export function t(key: string, options: TranslationOptions = {}) { - return resolveLocaleValue(key) ?? options.defaultValue ?? key; -} - -export function useTranslation() { - return { t }; -} +export const useTranslation = useReactI18nextTranslation; +export { i18n }; diff --git a/ui/src/i18n/locales.ts b/ui/src/i18n/locales.ts index c900f001fa9..188ee2b117d 100644 --- a/ui/src/i18n/locales.ts +++ b/ui/src/i18n/locales.ts @@ -1,3 +1,7 @@ +import type { Resource } from "i18next"; + +import { assertValidLocaleMessages } from "./locale-validation"; + export const DEFAULT_LOCALE = "en" as const; const localeModules = import.meta.glob("./locales/*.json", { @@ -19,4 +23,19 @@ if (!(DEFAULT_LOCALE in localeMessages)) { throw new Error(`Missing default locale messages for ${DEFAULT_LOCALE}`); } +for (const [locale, messages] of Object.entries(localeMessages)) { + try { + assertValidLocaleMessages(messages); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Invalid ${locale} locale messages: ${message}`); + } +} + +export const supportedLocales = Object.keys(localeMessages); + +export const i18nextResources: Resource = Object.fromEntries( + Object.entries(localeMessages).map(([locale, messages]) => [locale, { translation: messages }]), +) as Resource; + export type SupportedLocale = keyof typeof localeMessages;