Custom Plugins
Custom plugins are an advanced escape hatch. Most apps only need the official plugins: Fetch Loader, Locale Detector, and In-Context Editor.
Use a custom plugin when you need to register a custom loader, listen to i18n events, or add a post-processor.
Plugin Shape
Section titled “Plugin Shape”import type { I18nPluginFactory } from "@comvi/core";
export const MyPlugin: I18nPluginFactory<{ prefix?: string }> = (options = {}) => function MyPluginInstance(i18n) { const prefix = options.prefix ?? "[i18n]";
const unsubscribe = i18n.on("localeChanged", ({ from, to }) => { console.log(`${prefix} ${from} -> ${to}`); });
return unsubscribe; };Register plugins before init():
const i18n = createI18n({ locale: "en" }) .use(MyPlugin({ prefix: "[app]" }));
await i18n.init();The plugin function runs during init(). If it returns a function, that function runs during destroy().
Common Hooks
Section titled “Common Hooks”Custom Loader
Section titled “Custom Loader”registerLoader accepts either a function or a static import map.
Function form — called on demand for any locale/namespace pair:
const ApiLoader: I18nPluginFactory<{ apiUrl: string }> = (options) => { if (!options?.apiUrl) { throw new Error("ApiLoader requires apiUrl"); }
return (i18n) => { i18n.registerLoader(async (locale, namespace) => { const res = await fetch(`${options.apiUrl}/${locale}/${namespace}.json`); if (!res.ok) { throw new Error(`Failed to load ${locale}:${namespace}`); } return res.json(); }); };};Import-map form — static dynamic imports bundled at build time. Keys are "locale" (shorthand for the default namespace) or "locale:namespace":
const StaticLoader: I18nPluginFactory = () => (i18n) => { i18n.registerLoader({ en: () => import("./locales/en.json"), de: () => import("./locales/de.json"), "en:admin": () => import("./locales/en/admin.json"), "de:admin": () => import("./locales/de/admin.json"), });};Core resolves the import function for the requested locale:namespace key, falling back to the bare locale key for the default namespace. If no entry matches, it throws.
Event Listener
Section titled “Event Listener”const AnalyticsPlugin: I18nPluginFactory = () => (i18n) => { const unsubscribe = i18n.on("missingKey", ({ key, locale, namespace }) => { analytics.track("i18n_missing_key", { key, locale, namespace }); });
return unsubscribe;};Post-Processor
Section titled “Post-Processor”const UppercasePlugin: I18nPluginFactory = () => (i18n) => { i18n.registerPostProcessor((result, key, namespace, params) => { if (params.raw || typeof result !== "string") return result; return result.toUpperCase(); });};params.raw is only a convention. Core still calls post-processors; each processor decides whether to respect the flag.
Error Handling
Section titled “Error Handling”Factory errors happen immediately, before .use() receives a plugin:
const RequiredOptionPlugin: I18nPluginFactory<{ apiKey: string }> = (options) => { if (!options?.apiKey) { throw new Error("apiKey is required"); }
return (i18n) => { // ... };};Plugin-function errors happen during init() and are controlled by PluginOptions:
i18n.use(MyPlugin(), { required: false, timeout: 5000, onError: (error) => report(error),});Lifecycle Notes
Section titled “Lifecycle Notes”- Plugins run in registration order during
init(). - The registered locale detector runs after all plugins and before initial namespace loading.
- Cleanup functions run in reverse order during
destroy(). - Only one loader and one locale detector are active at a time; registering another replaces the previous one.
Available Events
Section titled “Available Events”Plugins can subscribe to any of the following events via i18n.on(event, handler). See Error Handling for payload shapes and error-handling patterns.
| Event | Fires when |
|---|---|
initialized | init() completed successfully |
destroyed | destroy() was called |
localeChanged | Active locale changed — payload { from, to } |
defaultNamespaceChanged | Default namespace changed — payload { from, to } |
translationsCleared | Cache cleared — payload { locale?, namespace? } |
loadingStateChanged | Loading state changed — payload { isLoading, isInitializing } |
namespaceLoaded | A namespace finished loading — payload { namespace, locale } |
missingKey | t() could not find a key — payload { key, locale, namespace } |
loadError | A namespace loader failed — payload { locale, namespace, error } |