# @comvi/core

API reference for @comvi/core: createI18n, I18nOptions, the i18n instance's methods and events, the free Intl functions, and the exported types.

Canonical: https://comvi.io/docs/i18n/api/core

`createI18n()` returns the i18n instance: text with `{param}` interpolation, the cache, events
and default params. The capabilities attach with `.use(installer)` from a subpath,
[`/loader`](/docs/i18n/api/core-loader), [`/plugins`](/docs/i18n/api/core-plugins) and
[`/devtools`](/docs/i18n/api/core-devtools); [`/icu`](/docs/i18n/api/core-icu) is the `compiler`
option and [`/rich-text` and `/tags`](/docs/i18n/api/core-rich-text) are the rich-text helpers.

## Exports

- Values: `createI18n`, `I18n`, `MessagesCache`, `flattenCatalog`, `createBoundTranslation`,
`translationResultToString`, `subscribeToRevision`, `REVISION_EVENTS`, `missingCapability`,
`hasLoaderApi`, `hasPluginApi`, `formatNumber`, `formatDate`, `formatCurrency`,
`formatRelativeTime`, `getTextDirection`, `createElement`, `isVirtualNode`.
- Types: `I18nOptions`, `I18nEvent`, `I18nEventData`, `TranslationKeys`, `TranslationParams`,
`TranslationResult`, `VirtualNode`, `ElementNode`, `TextNode`, `FragmentNode`,
`MessageCompiler`, `SyntaxExtension`, `IcuSyntaxError`, `LocaleSource`, `RevisionEvent`,
`RevisionEventSource`, `CapabilityName`, `I18nPlugin`, `I18nPluginFactory`, `PluginOptions`,
`ErrorReportContext`, `I18nBaseOptions`, `TranslationValue`, `MessagesMap`, `FlattenedMessages`, `LoaderFn`, `LoaderResult`, `MissingKeyInfo`, `PostProcessFn`,
`TagInterpolationOptions`, `TagCallbackParams`, `TranslationParamValue`, `DefaultTranslationParams`, `DefaultTranslationParamValue`, `SetDefaultParamsArg`, `DefaultParamsSnapshot`, `HasTranslationKeys`, `ExtractNamespaces`, the instance shapes and the key helpers listed under [Types](#types).

## `createI18n(options)`

```ts
function createI18n<D extends DefaultTranslationParams = {}>(options: I18nOptions<D>): I18n<D>
const I18n: new <D>(options: I18nOptions<D>) => I18n<D>
```

`createI18n(options)` is `new I18n(options)`. It throws when `locale` is missing or
`messages` is not an object; in development it also checks that every per-locale value is one.

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: { en: { greeting: 'Hello, {name}!' } },
});

await i18n.init();
```

### `I18nOptions`

| Option | Type | Default | Meaning |
| --- | --- | --- | --- |
| `locale` | `string` | required | Initial active locale |
| `defaultNs` | `string` | `'default'` | Namespace for a call that passes no `ns` param |
| `ns` | `string[]` | `[defaultNs]` | Namespaces `init()` loads; `[]` skips initial loading. Needs the loader capability, otherwise `init()` warns once in development |
| `messages` | `Record<string, Record<string, TranslationValue>>` | none | Inline catalogs keyed `'locale'` or `'locale:namespace'`, flat or nested |
| `compiler` | `MessageCompiler` | simple compiler | Pass `icuCompiler` for ICU plural, select and selectordinal |
| `fallbackLocale` | `string \| readonly string[]` | `[]` | Locales consulted, in order, when a key is missing or empty; with a loader, `init()` and each namespace activation load them too |
| `missingParam` | `'literal' \| 'drop'` | `'literal'` | Render an absent `{name}` as itself or as an empty string; `null` always renders empty |
| `defaultParams` | `DefaultTranslationParams` | none | Non-nullish interpolation defaults merged under every call; `locale`, `ns`, `fallback`, `markers`, `tagInterpolation` are rejected |
| `postProcess` | `PostProcessFn` | none | One post-processor, registered first |
| `onMissingKey` | `(info: MissingKeyInfo) => TranslationResult \| void` | none | Miss hook, consulted after the fallback chain when neither `params.fallback` nor a plugin callback supplied a value |
| `strict` | `'dev' \| 'off'` | `'off'` | `'dev'` warns on every miss in development builds |
| `apiKey` | `string` | none | Read back by plugins as `i18n.apiKey` |
| `collectContext` | `boolean` | `true` | `false` opts out of in-context editor context collection, even when the Chrome extension enables the editor |
| `tagInterpolation` | `TagInterpolationOptions` | none | `basicHtmlTags`, `strict`, `onTagWarning`, `extensions` |
| `devMode` | `boolean` | build flag | Defaults to the build flavour of the dist the bundler resolved: `true` from the `development` export condition, `false` from the default build. Plugins read it |
| `onError` | `(error: Error, context?: ErrorReportContext) => void` | none | Global error sink |
| `exposeGlobal` | `boolean` | none | Ignored by a bare instance, even one with `.use(devtools())`; `init()` warns once in development when `true`. Pass it to `devtools({ exposeGlobal })` instead |
| `instanceId` | `string` | none | Same: pass it to `devtools({ instanceId })` instead |

## `.use(installer)`

```ts
use<T extends this>(installer: (instance: this) => T): T
```

`i18n.use(f)` calls `f(i18n)` and returns the widened instance. The installer has to return
the instance it was given; anything else throws with `code: 'E_USE_NOT_INSTALLER'`. A raw
plugin goes through `definePlugin(fn)` or `registerPlugin(fn)` instead.

`src/compose.ts`

```ts

export const i18n = createI18n({ locale: 'en' })
.use(loader({ uk: () => import('./locales/uk.json') }))
.use(plugins());
```

## Properties

| Property | Type | Meaning |
| --- | --- | --- |
| `locale` | `string` (get/set) | The setter is fire-and-forget; a rejected switch emits `loadError` with `namespace: 'locale-change'` |
| `isLoading` | `boolean` | A load is in flight |
| `isInitializing` | `boolean` | `init()` is running |
| `isInitialized` | `boolean` | `init()` completed |
| `apiKey` | `string \| undefined` | Readonly, from the option |
| `collectContext` | `boolean \| undefined` | Readonly, from the option |
| `devMode` | `boolean` | Readonly |
| `instanceId` | `string \| undefined` | Assigned by the devtools capability |
| `messagesCache` | `MessagesCache` | Readonly, see [`MessagesCache`](#messagescache) |
| `defaultParams` | `DefaultParamsSnapshot<D>` | Readonly shallow snapshot, copied on every read |
| `configRevision` | `number` | Bumps on every `configChanged` |
| `localeRevision` | `number` | For framework bindings: how many times the locale has changed, bumped before `localeChanged` listeners run |

`isInitializing` belongs to `init()` alone: a locale detector that calls `setLocale()`
mid-init flips `isLoading`, never `isInitializing`. The flag clears when `init()` settles,
whether it resolved or threw.

## Methods

```ts
init(): Promise<this>
t(key, params?): string
tRaw(key, params?): TranslationResult
addMessages(messages: MessagesMap): void
getMessages(locale?: string, namespace?: string): FlattenedMessages
hasLocale(locale: string, namespace?: string): boolean
hasTranslation(key: string, locale?: string, namespace?: string, checkFallbacks?: boolean): boolean
setLocale(value: string): Promise<void>
setFallbackLocale(fallback: string | readonly string[]): void
setDefaultParams(params: SetDefaultParamsArg<D>): void
setDefaultNamespace(namespace: string): void
getDefaultNamespace(): string
getActiveNamespaces(): string[]
getFallbackLocales(): string[]
getLoadedLocales(): string[]
withMessages(catalogs: Readonly<Record<string, FlattenedMessages>>): I18nMessagesView<D>
clearMessages(locale?: string, namespace?: string): void
on<E extends I18nEvent>(event: E, callback: (data: I18nEventData[E]) => void): () => void
reportError(error: unknown, context?: ErrorReportContext): void
destroy(): Promise<void>
```

| Method | Meaning |
| --- | --- |
| `init()` | Runs plugin init hooks and the locale detector (plugin capability), then loads the initial namespaces. Rejects after `destroy()` |
| `t(key, params?)` | Translate to a string; `t(null)` returns `''`. One call signature: the key union is `AcceptedCoreKey`, params dispatch through `ArgsFor` |
| `tRaw(key, params?)` | Same lookup, returns `string \| Array<string \| VirtualNode>` |
| `addMessages(t)` | Merge catalogs keyed `'locale'` or `'locale:namespace'`, flat or nested; every namespace carried is activated and emits `namespaceLoaded` |
| `getMessages(l?, ns?)` | Defaults to the current locale and default namespace |
| `hasLocale(l, ns?)` | Cache check, no fallback chain |
| `hasTranslation(key, l?, ns?, fb?)` | Key check; an empty value is not a translation; `checkFallbacks` (default `false`) walks the chain |
| `setLocale(l)` | Synchronous with nothing composed; the loader capability overrides it to load first. A rapid second call supersedes the first silently |
| `setFallbackLocale(fb)` | Emits `configChanged` with source `'fallbackLocale'` |
| `setDefaultParams(p)` | Replaces defaults, emits `configChanged` with source `'defaultParams'`; constructor-guaranteed keys cannot be removed |
| `setDefaultNamespace(ns)` | Emits `defaultNamespaceChanged` |
| `getDefaultNamespace()`, `getActiveNamespaces()`, `getFallbackLocales()`, `getLoadedLocales()` | Snapshots, not live |
| `withMessages(catalogs)` | A read view with three methods and nothing else, `t`, `tRaw` and `hasTranslation`, which read each key from `catalogs` first and from the cache otherwise: the merge `addMessages(catalogs)` would make, without making it. The view never writes the instance (no cache entry, no locale, no active namespace) and never emits `namespaceLoaded` or `localeChanged`, so a dropped view leaves nothing behind. Diagnostics are not publication: a missing key emits `missingKey` and a throwing post-processor reaches `onError`, as for any `t()`. A later `addMessages()` reaches the view through the cache for every key `catalogs` does not hold. `catalogs` is keyed `'locale:namespace'` and holds flat catalogs, as `flattenCatalog` returns them |
| `clearMessages(l?, ns?)` | Clears content only, emits `messagesCleared`; deactivation is `removeActiveNamespace()` on the loader capability |
| `on(event, cb)` | Returns an unsubscribe function |
| `reportError(e, ctx?)` | Routes to `onError`; the same `Error` object is reported once per instance |
| `destroy()` | Idempotent. Removes discovery, runs plugin cleanups in LIFO order, emits `destroyed`, clears subscriptions, active namespaces, post-processors, cache, loader and detector |

A `destroy()` that lands while `init()` is awaiting makes `init()` resolve quietly with
`isInitialized` still `false`: no `initialized` event, no error report. That is what happens
when a component unmounts with a fire-and-forget `init()` in flight.

Lookup on a miss: emit `missingKey`, run the plugin `onMissingKey` callbacks, then return
`params.fallback` if set, else the first defined plugin result, else the constructor
`onMissingKey` result, else the key itself.

## Events

| Event | Payload | Fires when |
| --- | --- | --- |
| `initialized` | `void` | `init()` finished |
| `destroyed` | `void` | `destroy()` finished |
| `localeChanged` | `{ from: string; to: string }` | the locale changed |
| `defaultNamespaceChanged` | `{ from: string; to: string }` | the default namespace changed |
| `messagesCleared` | `{ locale?: string; namespace?: string }` | `clearMessages()` ran |
| `loadingStateChanged` | `{ isLoading: boolean; isInitializing: boolean }` | a loading flag flipped |
| `namespaceLoaded` | `{ namespace: string; locale: string }` | a catalog landed |
| `missingKey` | `{ key: string; locale: string; namespace: string }` | a lookup missed |
| `loadError` | `{ locale: string; namespace: string; error: Error }` | a load failed |
| `configChanged` | `{ source: 'fallbackLocale' \| 'namespaceActivated' \| 'namespaceDeactivated' \| 'defaultParams' }` | config changed |

`subscribeToRevision(i18n, cb)` subscribes to the seven events in `REVISION_EVENTS`
(`localeChanged`, `namespaceLoaded`, `loadingStateChanged`, `initialized`, `messagesCleared`,
`defaultNamespaceChanged`, `configChanged`) and returns one unsubscribe.

## Formatting functions

```ts
function formatNumber(i18n: LocaleSource, value: number, options?: Intl.NumberFormatOptions, locale?: string): string
function formatDate(i18n: LocaleSource, value: Date | number, options?: Intl.DateTimeFormatOptions, locale?: string): string
function formatCurrency(i18n: LocaleSource, value: number, currency: string, options?: Intl.NumberFormatOptions, locale?: string): string
function formatRelativeTime(i18n: LocaleSource, value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions, locale?: string): string
function getTextDirection(locale: string): 'ltr' | 'rtl'
```

Free functions, not methods. `LocaleSource` is `{ locale: string }`, so any i18n instance
qualifies as the first argument. `getTextDirection` takes a locale string. Formatters are
cached per locale and options.

## Helpers

```ts
function flattenCatalog(nested: Record<string, any>): FlattenedMessages
function createBoundTranslation<D, NS>(i18n: { t; tRaw? }, ns?: NS): NamespaceBoundTranslateFn<NS, D, TranslationResult> // any object with the instance's t(), such as the instance or a withMessages() view
function translationResultToString(result: TranslationResult): string
function subscribeToRevision(i18n: RevisionEventSource, callback: (event: RevisionEvent) => void): () => void
function missingCapability(name: 'loader' | 'plugins'): Error
function hasLoaderApi(instance: WrapperI18nInstance): instance is WrapperI18nInstance & I18nLoaderApi
function hasPluginApi(instance: WrapperI18nInstance): instance is WrapperI18nInstance & I18nPluginApi
function createElement(tag: string, props?: Record<string, unknown>, children?: Array<VirtualNode | string>): ElementNode
function isVirtualNode(value: unknown): value is VirtualNode
```

`flattenCatalog` is optional: the instance flattens nested catalogs itself. Non-string leaves
are coerced with `String()`, `null` and `undefined` leaves are dropped with a development
warning.

`hasLoaderApi` and `hasPluginApi` are runtime guards: each checks that every member of
the capability is a real function, so the development stand-ins `attachPlugins` installs for
loader members do not count.

## `MessagesCache`

```ts
new MessagesCache(options?: { defaultNs?: string })
get(locale: string, namespace?: string): FlattenedMessages | undefined
set(locale: string, namespace: string, translations: FlattenedMessages): void
has(locale: string, namespace?: string): boolean
delete(locale: string, namespace?: string): void
clear(): void
keys(): IterableIterator<string> // 'locale:namespace'
getLocales(): string[]
getRevision(): number
clone(): Map<string, FlattenedMessages>
getInternalMap(): ReadonlyMap<string, FlattenedMessages>
get size(): number
```

## Types

| Type | Meaning |
| --- | --- |
| `TranslationKeys` | Augment it to type `t()`; see [Type-check translation keys](/docs/i18n/type-safety) |
| `InferKeys<T, NS?, DefaultNS?>` | Keys inferred from a JSON catalog object, prefixed `NS:` unless `NS` equals `DefaultNS`; params are `never` |
| `DefaultNsKeys`, `Namespaces`, `NamespacedKeys<NS>`, `NamespacedKeyParams<NS, K>`, `PermissiveKey` | Key helpers derived from `TranslationKeys` |
| `ParamsArg`, `NamespacedParamsArg`, `NamespaceBoundParamsArg`, `AcceptedKey`, `AcceptedCoreKey`, `ArgsFor` | The single call signature's parts |
| `TranslateFn<D, R>`, `NamespaceBoundTranslateFn<NS, D, R>` | The `t` / `tRaw` shapes bindings return |
| `I18nCoreInstance<D>` | Lookup, cache and events surface |
| `I18nCoreExtraApi` | `init`, `destroy`, `setLocale`, `instanceId`, `withMessages`, the snapshot getters |
| `MessagesMap` | `Record<string, Record<string, TranslationValue>>`: catalogs keyed `'locale'` or `'locale:namespace'`, the shape `addMessages()` and the `messages` option take. Re-exported by `@comvi/react`, `@comvi/next` and `@comvi/nuxt` |
| `I18nMessagesView<D>` | What `withMessages()` returns: `t`, `tRaw` and `hasTranslation` of `I18nCoreInstance` |
| `WrapperI18nInstance<D>` | `I18nCoreInstance & I18nCoreExtraApi`, what every binding requires and a bare instance satisfies |
| `AnyI18nInstance` | `WrapperI18nInstance` with `setDefaultParams` and `init` widened, what providers accept |
| `I18nLoaderApi`, `I18nPluginApi` | The two capability member sets |
| `PluginI18nInstance<D>` | `WrapperI18nInstance & I18nLoaderApi & I18nPluginApi`, what a plugin may rely on |
| `I18nInstance<D>` | A composed instance: `I18nCoreInstance` plus `reloadMessages`, `setPluginData`, `getPluginData` |
| `VirtualNode` | `ElementNode \| TextNode \| FragmentNode`; `TranslationResult` is `string \| Array<string \| VirtualNode>` |
| `TagCallback` | `(params: { children: TranslationResult; name: string }) => VirtualNode \| string` |
| `IcuSyntaxError` | `Error & { code: 'E_ICU_SYNTAX'; argumentType: string }` |
| `ErrorReportContext` | `{ source; pluginName?; tagName?; key?; locale?; namespace?; event? }` with `source` one of `plugin`, `plugin-cleanup`, `request-cleanup`, `init`, `translation`, `namespace-load`, `post-processor`, `event`, `setLocale`, `compile` |
| `ComviQueue`, `ComviQueueEntry`, `ComviHook` | The `window.__COMVI__` protocol, see [`/devtools`](/docs/i18n/api/core-devtools) |

## Read next

- [`@comvi/core/loader`](/docs/i18n/api/core-loader): `loader(map)`, `attachLoader` and namespace activation.
- [`@comvi/core/plugins`](/docs/i18n/api/core-plugins): `plugins()`, `definePlugin()` and the plugin members.
- [Handle missing keys and errors](/docs/i18n/error-handling): `onError`, `reportError` and the error codes.
