Overview
With @comvi/core installed and an i18n instance from the Quick start,
add behaviour to it with a plugin: a loader, a locale detector, a post-processor, an event
listener. A plugin is a function of the i18n instance, and the official plugins and your own
use the same contract.
Compose a plugin
Section titled “Compose a plugin”import { createI18n } from '@comvi/core';import { definePlugin } from '@comvi/core/plugins';
export const i18n = createI18n({ locale: 'en' }).use( definePlugin( function LoggerPlugin(i18n) { return i18n.on('localeChanged', ({ from, to }) => { console.log(`[app] ${from} → ${to}`); }); }, { required: false }, ),);
await i18n.init();definePlugin(fn, options?) composes the plugin capability if the instance lacks it, queues
fn, and returns the instance, so your plugin is one .use() step. Every binding re-exports
definePlugin and plugins. An instance that composes .use(plugins()) also has
i18n.registerPlugin(fn, options?) for registering from a list. When a plugin registers a
loader, compose .use(attachLoader) or .use(loader(map)) as well; order does not matter,
because plugins run during init().
The contract
Section titled “The contract”.use(installer) is a pipe: i18n.use(f) is f(i18n). An installer receives the i18n
instance, attaches the capabilities its plugin needs, queues the plugin, and returns the same
instance with a wider type. It runs the moment you call .use(), which is before init(),
and it must hand back the object it was given; anything else is refused with
E_USE_NOT_INSTALLER. The plugin function it queued runs later, inside init(), with the
instance as its one argument, and returns nothing or a cleanup function. The split is what
lets an installer widen the type: by the time init() runs, every .use() has already
declared what members the instance has.
Two names for one plugin
Section titled “Two names for one plugin”| Package | Installer, for .use() |
Plugin factory, for registerPlugin() |
|---|---|---|
@comvi/plugin-fetch-loader |
cdnLoader(options) |
cdnLoaderPlugin(options) |
@comvi/plugin-fetch-loader/api |
fetchLoader(options) |
fetchLoaderPlugin(options) |
@comvi/plugin-locale-detector |
localeDetector(options) |
localeDetectorPlugin(options) |
@comvi/plugin-in-context-editor |
inContextEditor(options) |
inContextEditorPlugin(options) |
import { createI18n } from '@comvi/core';import { attachLoader } from '@comvi/core/loader';import { plugins } from '@comvi/core/plugins';import { cdnLoaderPlugin } from '@comvi/plugin-fetch-loader';
export const i18n = createI18n({ locale: 'en' }).use(attachLoader).use(plugins());
i18n.registerPlugin( cdnLoaderPlugin({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }), { required: false },);
await i18n.init();The installer composes the capabilities its plugin needs and then registers it, so it works
on a bare instance. The Plugin-suffixed factory only registers, on an instance you composed
yourself, and it is the form that takes registration options. The two slots are guarded.
registerPlugin(installer(…)) throws E_INSTALLER_AS_PLUGIN at init(), before anything is
attached, and .use(plugin(…)) throws E_USE_NOT_INSTALLER on the spot. The second refusal
comes after the call, so the plugin body has already run and its cleanup is lost. See
Write a plugin.
Lifecycle
Section titled “Lifecycle”registerPlugin() only queues. init() drains the queue once:
- plugin functions run in registration order, each under its own timeout;
- the registered locale detector runs and may switch the locale through
setLocale(), so its namespaces load before the locale applies; - the initial namespaces load through whatever loader the plugins registered;
- on
destroy(), cleanup functions run in reverse order and are awaited.
A plugin registered from inside another plugin still runs; one queued after init() waits for
the next init(), which drains the whole queue again, and logs one development warning. A plugin may return nothing or a cleanup function;
any other return value throws E_PLUGIN_INIT_RETURN and registers nothing.
Registration options
Section titled “Registration options”| Option | Default | Behaviour |
|---|---|---|
required |
true |
A throw or timeout aborts init(). Set false to report the error and continue. |
timeout |
10000 ms |
Budget for the plugin function. Exceeding it rejects with a timeout error. |
onError |
none | Called before the default error pipeline when the plugin fails. A throw inside it is swallowed. |
import { createI18n } from '@comvi/core';import { plugins } from '@comvi/core/plugins';import { localeDetectorPlugin } from '@comvi/plugin-locale-detector';
declare function captureException(error: Error): void;
export const i18n = createI18n({ locale: 'en' }).use(plugins());
i18n.registerPlugin(localeDetectorPlugin({ supportedLocales: ['en', 'uk'] }), { required: false, timeout: 5000, onError: (error) => captureException(error),});
await i18n.init();Required or not, a failure reaches onError with { source: 'plugin', pluginName }. Name
your plugin functions, or the report says "anonymous". A factory that throws on invalid
options fails at the call site, before any of these options apply, which is where you want a
typo in cdnUrl to surface.
Read next
Section titled “Read next”- Fetch loader: load translations from the Comvi CDN or API.
- Locale detector: detect and persist the user’s locale.
- Write a plugin: your own plugin or installer.