Translating a plugin
This is how a plugin ships its own translated UI strings in every language we bundle
(en, de, es, fr, pt). For a working example, look at the xentium-advanced-moderation
plugin (namespace advmod).
How it works
Section titled “How it works”Your plugin bundles its locale JSON into its web bundle and registers it with the
host’s i18next instance when it loads. There’s no HTTP fetch and nothing to package on
the server; the translations travel inside web/dist/index.umd.js. Your plugin owns a
namespace. Use the plugin id or a short slug, e.g. forum, messaging, advmod.
If the strings you need already exist in a core namespace (e.g. acp), you can
just use that one and skip the locale files entirely. Our moderation cases UI does this.
Step by step
Section titled “Step by step”-
Add the locale files, one JSON per language, named after your namespace:
web/locales/en/<ns>.jsonweb/locales/de/<ns>.jsonweb/locales/es/<ns>.jsonweb/locales/fr/<ns>.jsonweb/locales/pt/<ns>.jsonweb/locales/en/forum.json { "thread": { "reply": "Reply", "solved": "Solved" } } -
Externalize the bridge in
vite.config.ts, next to the others:"@web/plugins/pluginI18n": `${H}.webPluginI18n`, -
Add the dev dependencies for typechecking. They’re externalized at runtime, so only
tscneeds them:// package.json devDependencies"i18next": "^26.2.0","react-i18next": "^17.0.8"Then run
pnpm install --filter <your-plugin>. (web/tsconfig.jsonalready hasresolveJsonModule: true.) -
Register them in
web/index.tsx, before any UI renders:import { registerPluginTranslations } from "@web/plugins/pluginI18n";import en from "./locales/en/forum.json";import de from "./locales/de/forum.json";// …es, fr, ptregisterPluginTranslations({en: { forum: en }, de: { forum: de },es: { forum: es }, fr: { forum: fr }, pt: { forum: pt },}); -
Use them in your components:
import { useTranslation } from "react-i18next";const { t } = useTranslation("forum");return <button>{t("thread.reply")}</button>;
A missing key falls back to English (i18next’s fallbackLng). Switching the UI language
doesn’t fetch anything, because every language is registered up front. To use a key from
another namespace, prefix it: t("common:actions.cancel").
What core gives you
Section titled “What core gives you”registerPluginTranslations(resources)inapps/web/src/plugins/pluginI18n.ts, whereresourcesis{ [lng]: { [ns]: { …keys } } }. It’s exposed aswindow.__XENTIUM_HOST__.webPluginI18n.react-i18nextitself is shared too (ReactI18next), so your plugin uses the host’s i18next instance and everything it has already loaded.
What’s not covered yet
Section titled “What’s not covered yet”Translating a plugin’s emails and notifications on the server is a separate thing, and we haven’t built it for plugins yet. This page is only about strings in the web UI.