Skip to content

@comvi/core API Reference

@comvi/core is the framework-agnostic foundation for Comvi i18n. All framework packages (@comvi/vue, @comvi/react, etc.) build on top of it and re-export its APIs.

Creates a new i18n instance.

import { createI18n } from '@comvi/core';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
});
OptionTypeDefaultDescription
localestringInitial active locale (required). E.g., 'en', 'fr-CA'
defaultNsstring'default'Fallback namespace for keys without ns: prefix
nsstring[]Namespaces to load during init(). If omitted, only defaultNs loads
translationRecord<string, Record<string, TranslationValue>>Initial in-memory translations. Keys: 'locale' (default ns) or 'locale:namespace'
fallbackLocalestring | string[]Chain of locales to check on missing key
postProcessPostProcessFnGlobal post-processor (FIFO)
onMissingKey(info: MissingKeyInfo) => TranslationResult | voidHook for missing keys. Return value overrides fallback chain
strict'dev' | 'off''off'Strict diagnostics mode
apiKeystringToken for plugin auth (i18n.apiKey)
exposeGlobalbooleantrue (browser)Register on window.__COMVI__
instanceIdstringautocomvi-${counter} if omitted
tagInterpolationTagInterpolationOptionsRich-text/tag config
devModebooleanautoWhen omitted, defaults to process?.env?.NODE_ENV !== 'production' (bundlers replace process.env.NODE_ENV at build time). Plugins read it to decide behavior (e.g. API vs CDN loading)
onError(error, ctx?) => voidGlobal error handler
const i18n = createI18n({
locale: 'en',
fallbackLocale: ['en', 'de'],
defaultNs: 'common',
ns: ['common', 'dashboard'],
translation: {
en: { 'hello': 'Hello', 'world': 'World' },
},
onMissingKey: ({ key, locale, namespace }) => `[missing: ${key}]`,
onError: (error, ctx) => Sentry.captureException(error, { extra: ctx }),
});

Get or set the current language. Setting triggers translation loading and re-renders in framework bindings.

console.log(i18n.locale); // 'en'
i18n.locale = 'de'; // Switches to German (fire-and-forget)

boolean — Whether translations are currently being loaded.

boolean — Whether Comvi i18n is currently running init().

boolean — Whether init() has completed successfully.

string | undefined — The API key configured for this instance. Plugins use this to authenticate with backend services.

boolean — Whether Comvi i18n is running in development mode. When the devMode option is omitted, this is derived from process?.env?.NODE_ENV !== 'production' (bundlers like Vite, webpack, and Turbopack inline process.env.NODE_ENV at build time). Plugins use it to determine behavior (e.g., API vs CDN loading).

TranslationCache — Direct access to the underlying translation cache. For advanced use cases only.

Initialize the i18n instance. Runs all registered plugins (FIFO, each wrapped in a timeout race), runs the registered locale detector if any, and loads the initial namespaces for the active locale. Returns the instance, so you can register plugins and initialize in one chained expression.

await i18n.init();
// Chained form — init() resolves to the I18n instance
export const i18n = await createI18n({ locale: 'en' })
.use(FetchLoader({ cdnUrl: '...' }))
.init();

Calling init() after destroy() throws — create a new instance instead.

Signature: init(): Promise<this>

Translate a key to a plain string. Type-safe when TranslationKeys is declared. Full syntax in Translation Function.

i18n.t('hello.world'); // "Hello, World!"
i18n.t(null); // returns "" when the key is null
i18n.t('greeting', { name: 'Alice' });
i18n.t('count', { count: 5, ns: 'admin' }); // with namespace
i18n.t('key', { fallback: 'Fallback text' }); // fallback overrides any onMissingKey return value
i18n.t('msg', { raw: true }); // post-processors that support it skip this call

Lookup chain: current locale → fallback locale chain → emit 'missingKey' event → runtime onMissingKey callbacks → option onMissingKey callback → if params.fallback is set, return its interpolated text (this overrides callback return values) → otherwise the first callback return value, or the key string itself.

Translate a key to a raw TranslationResult (string | Array<string | VirtualNode>). Use this only for custom rich-text renderers or framework integration code; regular UI text should use t().

Register a plugin. Returns the instance for chaining.

i18n
.use(FetchLoader({ cdnUrl: '...' }))
.use(LocaleDetector({ order: ['cookie', 'navigator'] }));

Plugin options:

OptionTypeDefaultDescription
requiredbooleantrueWhether a plugin failure should abort initialization
timeoutnumber10000Plugin initialization timeout in milliseconds
onError(error: Error) => voidCustom error handler for this plugin
// Non-critical plugin that won't block init if it fails
i18n.use(InContextEditorPlugin(options), { required: false, timeout: 5000 });

Add translations at runtime. The argument is an object keyed by language, where each value is an object of key-value translation pairs.

i18n.addTranslations({
en: { 'new.key': 'New value', 'another.key': 'Another value' },
de: { 'new.key': 'Neuer Wert' },
});

i18n.getTranslations(language?, namespace?)

Section titled “i18n.getTranslations(language?, namespace?)”

Get loaded translations for a language and namespace. Defaults to the current language and default namespace.

const translations = i18n.getTranslations();
const german = i18n.getTranslations('de');
const germanDash = i18n.getTranslations('de', 'dashboard');

Check if a locale (optionally a namespace) is loaded in cache. Existence check, no lookup.

i18n.hasLocale('de'); // boolean
i18n.hasLocale('de', 'dashboard'); // boolean

i18n.hasTranslation(key, locale?, namespace?, checkFallbacks?)

Section titled “i18n.hasTranslation(key, locale?, namespace?, checkFallbacks?)”

Check if a specific key exists in cache. No lookup or interpolation.

i18n.hasTranslation('hello.world');
i18n.hasTranslation('hello.world', 'de');
i18n.hasTranslation('hello.world', 'de', 'common');
i18n.hasTranslation('hello.world', undefined, undefined, true); // check fallback chain

Set language and wait for translations to load before resolving. Handles race conditions when called rapidly.

await i18n.setLocaleAsync('de');
// Translations are guaranteed to be loaded at this point

Update fallback locales at runtime.

i18n.setFallbackLocale('en');
i18n.setFallbackLocale(['en', 'de']); // Chain of fallbacks

Update the default namespace at runtime.

i18n.setDefaultNamespace('dashboard');

i18n.clearTranslations(language?, namespace?)

Section titled “i18n.clearTranslations(language?, namespace?)”

Clear translations from cache.

i18n.clearTranslations(); // Clear everything
i18n.clearTranslations('de'); // Clear all German translations
i18n.clearTranslations('de', 'dashboard'); // Clear specific language + namespace

i18n.reloadTranslations(language?, namespace?)

Section titled “i18n.reloadTranslations(language?, namespace?)”

Reload translations from the registered loader.

await i18n.reloadTranslations(); // Reload current language + active namespaces
await i18n.reloadTranslations('de'); // Reload specific language
await i18n.reloadTranslations('de', 'dashboard');

Load and activate a namespace dynamically. The namespace is fetched via the registered loader and added to the active namespace list.

await i18n.addActiveNamespace('settings');
// "settings" namespace is now loaded and active

Load and activate multiple namespaces at once.

await i18n.addActiveNamespaces(['settings', 'billing']);

Returns the list of currently active namespaces.

const active = i18n.getActiveNamespaces(); // ['common', 'dashboard']

Returns the default namespace.

const defaultNs = i18n.getDefaultNamespace(); // 'common'

Returns all languages that have translations loaded in the cache.

const languages = i18n.getLoadedLocales(); // ['en', 'de']

Report an error to the configured onError handler. Use for custom error reporting in plugins or application code.

i18n.reportError(new Error('Custom error'), {
source: 'plugin',
pluginName: 'my-plugin',
});

Tear down the instance and release all resources. It is idempotent — calling it twice is a no-op.

await i18n.destroy();

destroy():

  • Unregisters the instance from window.__COMVI__
  • Runs all plugin cleanup functions in LIFO order (the reverse of registration)
  • Resets the lifecycle flags (isLoading, isInitializing, isInitializedfalse) and emits 'loadingStateChanged' then 'destroyed'
  • Clears event subscriptions, onMissingKey callbacks, the registered loader, post-processors, the translation cache, the locale detector, and the Intl.* formatter caches

Use it for SPA route teardown, micro-frontend unmounts, and between test cases that each build a fresh instance. After destroy(), calling init() on the same instance throws — create a new instance instead.

Signature: destroy(): Promise<void>

Subscribe to i18n events. Returns an unsubscribe function.

const unsub = i18n.on('localeChanged', ({ from, to }) => {
console.log(`Language changed from ${from} to ${to}`);
});
// Later:
unsub();
EventPayloadNotes
initializedvoidInstance finished init()
destroyedvoidInstance was destroy()ed
localeChanged{ from: string; to: string }Language was changed
defaultNamespaceChanged{ from: string; to: string }Default namespace was changed
translationsCleared{ locale?: string; namespace?: string }Cache cleared (no args = all)
loadingStateChanged{ isLoading: boolean; isInitializing: boolean }Loading or initializing state changed
namespaceLoaded{ namespace: string; locale: string }Namespace fetched for a locale
missingKey{ key: string; locale: string; namespace: string }Key not found in lookup
loadError{ locale: string; namespace: string; error: Error }Load failure
// Listen for missing keys to report to your TMS
i18n.on('missingKey', ({ key, locale, namespace }) => {
reportMissingKey(key, locale, namespace);
});
// Track loading state for UI spinners
i18n.on('loadingStateChanged', ({ isLoading }) => {
showSpinner(isLoading);
});
// Handle load errors
i18n.on('loadError', ({ locale, namespace, error }) => {
console.error(`Failed to load ${locale}/${namespace}:`, error);
});
type PluginCleanup = () => void | Promise<void>;
type I18nPlugin = (i18n: I18n) => void | Promise<void> | PluginCleanup | Promise<PluginCleanup>;
type I18nPluginFactory<T = unknown> = (options?: T) => I18nPlugin;
interface PluginOptions {
required?: boolean; // default true: throw on failure; false: log & continue
timeout?: number; // default 10000ms
onError?: (error: Error) => void;
}

Plugins run sequentially (FIFO) during init(), wrapped in Promise.race(timeout). Cleanups run in LIFO on destroy().

Inside a plugin function, register hooks on the i18n instance:

  • i18n.registerLoader(loader) — Function or import map for loading translations

    // Import map (Vite/Webpack-compatible):
    i18n.registerLoader({
    'en': () => import('./locales/en/default.json'),
    'en:admin': () => import('./locales/en/admin.json'),
    });
    // OR function:
    i18n.registerLoader(async (locale, ns) => fetchJson(...));

    Only ONE loader at a time; subsequent calls replace previous.

  • i18n.registerLocaleDetector(fn) — Detects locale during init()

    i18n.registerLocaleDetector(() => {
    return localStorage.getItem('lang') || navigator.language;
    });

    Throws if argument is not a function.

  • i18n.registerPostProcessor(fn) — Multiple allowed (FIFO)

    type PostProcessFn = (result: TranslationResult, key: string, ns: string, params: TranslationParams) => TranslationResult;
    i18n.registerPostProcessor((result, key, ns, params) => {
    // Custom post-processing
    return result;
    });
  • i18n.onMissingKey(cb) — Multiple callbacks; first non-undefined wins. Returns unsubscribe.

  • i18n.onLoadError(cb) — Error callback for loads. Returns unsubscribe.

  • i18n.setPluginData(key, data) / i18n.getPluginData<T>(key) — Plugin-scoped state storage

const MyPlugin = (options: { apiUrl: string }): I18nPlugin => (i18n) => {
i18n.registerLoader(async (locale, ns) => {
const res = await fetch(`${options.apiUrl}/${locale}/${ns}.json`);
return res.json();
});
const unsub = i18n.on('localeChanged', ({ to }) => {
console.log(`Language changed to ${to}`);
});
return () => unsub(); // cleanup
};

For the first-party plugins, see Plugins Overview.

Access via i18n.translationCache (readonly). Public methods:

get(locale: string, ns?: string): FlattenedTranslations | undefined;
set(locale: string, ns: string, translations: FlattenedTranslations): void;
has(locale: string, ns?: string): boolean;
delete(locale: string, ns?: string): void;
clear(): void;
getLocales(): string[];
getRevision(): number; // Bumps on any mutation; O(1) change detection
clone(): Map<string, FlattenedTranslations>; // 'locale:namespace' keys

Declare translation keys via module augmentation:

declare module '@comvi/core' {
interface TranslationKeys {
'common.welcome': never; // no params
'common.greeting': { name: string }; // required param
'errors.count': { count: number };
'admin:dashboard.title': never; // namespaced (uses ':')
}
}

Exported helpers:

  • DefaultNsKeys, Namespaces, NamespacedKeys<NS>, NamespacedKeyParams<NS, K>, ParamsArg<K>, NamespacedParamsArg<NS, K>
  • InferKeys<T, NS?, DefaultNS?> — infer keys from a JSON locale object. With NS, keys get a NS: prefix; if NS equals DefaultNS the prefix is dropped. Params are not inferred — every key gets never (params optional). See Type-Safe Translations › Manual type augmentation from local JSON.

Framework packages re-export types; augmenting @comvi/core suffices for all bindings.

interface ElementNode {
type: 'element';
tag: string;
props: Record<string, unknown>;
children: Array<VirtualNode | string>;
key?: string | number;
}
interface TextNode { type: 'text'; text: string; }
interface FragmentNode {
type: 'fragment';
children: Array<VirtualNode | string>;
key?: string | number;
}
type VirtualNode = ElementNode | TextNode | FragmentNode;
type TranslationResult = string | Array<string | VirtualNode>;

Exported helper: createElement(tag, props, ...children).

When exposeGlobal: true (default in browser), each instance registers on window.__COMVI__:

interface ComviGlobal {
version: string;
instances: Map<string, I18n>;
register(id: string, instance: I18n): void;
unregister(id: string): void;
get(id?: string): I18n | undefined; // default = first instance
onInstanceRegistered?: (id: string, instance: I18n) => void;
}

Browser fires window.dispatchEvent(new CustomEvent('COMVI_READY', { detail: { version, instanceCount, instanceId } })) on registration.

  1. i18n.locale = 'fr' is fire-and-forget — use setLocaleAsync('fr') to await translation loading. The I18n class has no setLocale() method; that name belongs to the framework binding hooks (useI18n() in vue/react/solid/svelte/nuxt).
  2. params.fallback overrides onMissingKey return values — on a miss, the onMissingKey callbacks always run (use them for diagnostics), but if params.fallback is set its interpolated text is returned regardless of what the callbacks returned.
  3. Post-processors run FIFO — first registered runs first.
  4. Plugin cleanups run LIFO in destroy().
  5. Import-map loader keys without : expand to 'locale:defaultNs'.
  6. Apostrophes: inside word boundaries are literals; standalone ' is ICU quote delimiter.
  7. Static template fast-path: no params + no post-procs + no special chars → string returned as-is.
  8. setFallbackLocale(["en", "de"]) accepts array (priority order) or single string.
  9. params.raw === true is passed to post-processors. Post-processors that support it can opt out, for example the in-context editor marker injector.
  10. Formatter caching: Intl.*Format cached per locale + JSON.stringify(options). Each formatter method accepts an optional trailing locale argument (formatNumber(value, options?, locale?), formatDate(value, options?, locale?), formatCurrency(value, currency, options?, locale?), formatRelativeTime(value, unit, options?, locale?)) that overrides the instance locale for that single call; omitting it uses the active locale.
  11. dir getter: uses Intl.Locale.textInfo (ES2023+); falls back to hardcoded RTL list.