Skip to content

Commit 87cf736

Browse files
authored
feat(react-ui): add multilingual (i18n) support (#9642)
Adds end-to-end internationalization to the React UI with five seed languages (English, Italian, Spanish, German, Simplified Chinese) and a sidebar-footer language switcher next to the existing theme toggle. Library: react-i18next + i18next + i18next-http-backend + i18next-browser-languagedetector. The detector caches the user's choice in localStorage (key `localai-language`, mirroring the existing `localai-theme` convention) and updates the `<html lang>` attribute on change. fallbackLng is `en`, so any missing translation in another locale falls back transparently. Translation files live under `public/locales/<lng>/<ns>.json`. They ride along with the existing `//go:embed react-ui/dist/*` directive, but the previous SPA route in core/http/app.go only exposed `/assets/*` from the embedded React build. This commit generalizes the asset handler into a `serveReactSubdir(subdir)` helper and adds a matching `/locales/*` route so i18next-http-backend can fetch the JSONs at runtime. The http-backend `loadPath` is built via the existing `apiUrl()` helper so instances served under a sub-path (e.g. `<base href="/ui/">`) resolve correctly. Namespaces (13): common, nav, errors, auth, home, models, importModel, chat, agents, skills, collections, media, admin. Translated UI surfaces include the sidebar/header/footer chrome, login + account flows, the Home dashboard (incl. the manage-by-chat assistant CTA), the model gallery + import flow, the chat experience (Chat.jsx + ChatsMenu), agents/skills/collections list pages, the studio media tabs (Image, Video, TTS), and the admin page-headers (Settings incl. its section nav, Manage, Backends, Traces, Nodes, P2P, Users, Usage). Shared components (ConfirmDialog, Toast) take their default labels from the common namespace so callers don't need to pass strings explicitly. Tooling for incremental adoption is included: - `i18next-parser.config.js` + `npm run i18n:extract` to sweep `t()` keys into the JSON skeletons. - `scripts/translate-locales.mjs` (one-off helper) to bootstrap non-English locales from English source via OpenAI or Anthropic APIs, with --copy mode as a placeholder fallback. Idempotent; preserves existing translations unless --overwrite is passed. Larger config-driven pages (ModelEditor, Settings deep field forms, AgentChat/AgentCreate, SkillEdit, CollectionDetails, Talk, Sound, biometrics, FineTune/Quantize, Users modals, Nodes/P2P install pickers, BackendLogs, Traces deep filters, Explorer) intentionally keep their inner content untranslated for now — they fall back to English via fallbackLng so functionality is unaffected, and the extracted-strings pattern + the bootstrap script make follow-up extraction straightforward. The initial Suspense fallback at the root in main.jsx covers the first JSON fetch on cold load. A simple `.app-boot-spinner` styled in App.css provides a non-empty paint while the first namespace loads. Assisted-by: Claude:claude-opus-4-7 [Bash Read Edit Write Agent] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 1ad5b59 commit 87cf736

101 files changed

Lines changed: 8616 additions & 732 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/http/app.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -447,24 +447,28 @@ func API(application *application.Application) (*echo.Echo, error) {
447447
return prefixRedirect(c, "/app/"+p)
448448
})
449449

450-
// Serve React static assets (JS, CSS, etc.)
451-
serveReactAsset := func(c echo.Context) error {
452-
p := "assets/" + c.Param("*")
453-
f, err := reactFS.Open(p)
454-
if err == nil {
455-
defer f.Close()
456-
stat, statErr := f.Stat()
457-
if statErr == nil && !stat.IsDir() {
458-
contentType := mime.TypeByExtension(filepath.Ext(p))
459-
if contentType == "" {
460-
contentType = echo.MIMEOctetStream
450+
// Serve React static assets (JS, CSS, etc.) and i18n locale JSONs
451+
// from the embedded React build.
452+
serveReactSubdir := func(subdir string) echo.HandlerFunc {
453+
return func(c echo.Context) error {
454+
p := subdir + "/" + c.Param("*")
455+
f, err := reactFS.Open(p)
456+
if err == nil {
457+
defer f.Close()
458+
stat, statErr := f.Stat()
459+
if statErr == nil && !stat.IsDir() {
460+
contentType := mime.TypeByExtension(filepath.Ext(p))
461+
if contentType == "" {
462+
contentType = echo.MIMEOctetStream
463+
}
464+
return c.Stream(http.StatusOK, contentType, f)
461465
}
462-
return c.Stream(http.StatusOK, contentType, f)
463466
}
467+
return echo.NewHTTPError(http.StatusNotFound)
464468
}
465-
return echo.NewHTTPError(http.StatusNotFound)
466469
}
467-
e.GET("/assets/*", serveReactAsset)
470+
e.GET("/assets/*", serveReactSubdir("assets"))
471+
e.GET("/locales/*", serveReactSubdir("locales"))
468472
}
469473
}
470474
routes.RegisterJINARoutes(e, requestExtractor, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig())
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default {
2+
locales: ['en', 'it', 'es', 'de', 'zh-CN'],
3+
defaultNamespace: 'common',
4+
output: 'public/locales/$LOCALE/$NAMESPACE.json',
5+
input: ['src/**/*.{js,jsx}'],
6+
keySeparator: '.',
7+
namespaceSeparator: ':',
8+
defaultValue: (locale, _ns, key) => (locale === 'en' ? key : ''),
9+
sort: true,
10+
createOldCatalogs: false,
11+
keepRemoved: false,
12+
}

0 commit comments

Comments
 (0)