Skip to content
Comvi

Overview

View .md

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.

src/i18n.ts
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().

.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.

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)
src/i18n.ts
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.

registerPlugin() only queues. init() drains the queue once:

  1. plugin functions run in registration order, each under its own timeout;
  2. the registered locale detector runs and may switch the locale through setLocale(), so its namespaces load before the locale applies;
  3. the initial namespaces load through whatever loader the plugins registered;
  4. 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.

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.
src/i18n.ts
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.