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
devModebooleanautoFrom import.meta.env.DEV or NODE_ENV !== 'production'
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. Plugins use this 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, detects language, and loads initial translations.

await i18n.init();

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

i18n.t('hello.world'); // null key returns ""
i18n.t('greeting', { name: 'Alice' });
i18n.t('count', { count: 5, ns: 'admin' }); // with namespace
i18n.t('key', { fallback: 'Fallback text' }); // fallback checked after onMissingKey

Lookup chain: current locale → fallback chain → onMissingKey callbacks → params.fallback → key string itself. Emits 'missingKey' event on miss.

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',
});

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?> — auto-infer from JSON (params default to never)

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.
  2. params.fallback checked AFTER onMissingKey — missing key calls callbacks first, then tries fallback string.
  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).
  11. dir getter: uses Intl.Locale.textInfo (ES2023+); falls back to hardcoded RTL list.