# Overview

How a Comvi plugin is composed with .use(), the installer and plugin-factory names every package exports, the init() lifecycle and the registration options.

Canonical: https://comvi.io/docs/i18n/plugins

With `@comvi/core` installed and an i18n instance from the [Quick start](/docs/i18n/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

`src/i18n.ts`

```ts

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

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

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

```ts

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](/docs/i18n/plugins/custom-plugins#the-mirror-mistake).

## Lifecycle

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

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

`src/i18n.ts`

```ts

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

- [Fetch loader](/docs/i18n/plugins/fetch-loader): load translations from the Comvi CDN or API.
- [Locale detector](/docs/i18n/plugins/locale-detector): detect and persist the user's locale.
- [Write a plugin](/docs/i18n/plugins/custom-plugins): your own plugin or installer.
