@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.
createI18n(options)
Section titled “createI18n(options)”Creates a new i18n instance.
import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en', fallbackLocale: 'en',});I18nOptions
Section titled “I18nOptions”| Option | Type | Default | Description |
|---|---|---|---|
locale | string | — | Initial active locale (required). E.g., 'en', 'fr-CA' |
defaultNs | string | 'default' | Fallback namespace for keys without ns: prefix |
ns | string[] | — | Namespaces to load during init(). If omitted, only defaultNs loads |
translation | Record<string, Record<string, TranslationValue>> | — | Initial in-memory translations. Keys: 'locale' (default ns) or 'locale:namespace' |
fallbackLocale | string | string[] | — | Chain of locales to check on missing key |
postProcess | PostProcessFn | — | Global post-processor (FIFO) |
onMissingKey | (info: MissingKeyInfo) => TranslationResult | void | — | Hook for missing keys. Return value overrides fallback chain |
strict | 'dev' | 'off' | 'off' | Strict diagnostics mode |
apiKey | string | — | Token for plugin auth (i18n.apiKey) |
exposeGlobal | boolean | true (browser) | Register on window.__COMVI__ |
instanceId | string | auto | comvi-${counter} if omitted |
tagInterpolation | TagInterpolationOptions | — | Rich-text/tag config |
devMode | boolean | auto | From import.meta.env.DEV or NODE_ENV !== 'production' |
onError | (error, ctx?) => void | — | Global 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 }),});Instance Properties
Section titled “Instance Properties”i18n.locale
Section titled “i18n.locale”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)i18n.isLoading
Section titled “i18n.isLoading”boolean — Whether translations are currently being loaded.
i18n.isInitializing
Section titled “i18n.isInitializing”boolean — Whether Comvi i18n is currently running init().
i18n.isInitialized
Section titled “i18n.isInitialized”boolean — Whether init() has completed successfully.
i18n.apiKey
Section titled “i18n.apiKey”string | undefined — The API key configured for this instance. Plugins use this to authenticate with backend services.
i18n.devMode
Section titled “i18n.devMode”boolean — Whether Comvi i18n is running in development mode. Plugins use this to determine behavior (e.g., API vs CDN loading).
i18n.translationCache
Section titled “i18n.translationCache”TranslationCache — Direct access to the underlying translation cache. For advanced use cases only.
Instance Methods
Section titled “Instance Methods”i18n.init()
Section titled “i18n.init()”Initialize the i18n instance. Runs all registered plugins, detects language, and loads initial translations.
await i18n.init();i18n.t<K>(key, ...params)
Section titled “i18n.t<K>(key, ...params)”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 namespacei18n.t('key', { fallback: 'Fallback text' }); // fallback checked after onMissingKeyLookup chain: current locale → fallback chain → onMissingKey callbacks → params.fallback → key string itself. Emits 'missingKey' event on miss.
i18n.tRaw<K>(key, ...params)
Section titled “i18n.tRaw<K>(key, ...params)”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().
i18n.use(plugin, options?)
Section titled “i18n.use(plugin, options?)”Register a plugin. Returns the instance for chaining.
i18n .use(FetchLoader({ cdnUrl: '...' })) .use(LocaleDetector({ order: ['cookie', 'navigator'] }));Plugin options:
| Option | Type | Default | Description |
|---|---|---|---|
required | boolean | true | Whether a plugin failure should abort initialization |
timeout | number | 10000 | Plugin initialization timeout in milliseconds |
onError | (error: Error) => void | — | Custom error handler for this plugin |
// Non-critical plugin that won't block init if it failsi18n.use(InContextEditorPlugin(options), { required: false, timeout: 5000 });i18n.addTranslations(translations)
Section titled “i18n.addTranslations(translations)”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');i18n.hasLocale(locale, namespace?)
Section titled “i18n.hasLocale(locale, namespace?)”Check if a locale (optionally a namespace) is loaded in cache. Existence check, no lookup.
i18n.hasLocale('de'); // booleani18n.hasLocale('de', 'dashboard'); // booleani18n.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 chaini18n.setLocaleAsync(locale)
Section titled “i18n.setLocaleAsync(locale)”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 pointi18n.setFallbackLocale(fallback)
Section titled “i18n.setFallbackLocale(fallback)”Update fallback locales at runtime.
i18n.setFallbackLocale('en');i18n.setFallbackLocale(['en', 'de']); // Chain of fallbacksi18n.setDefaultNamespace(namespace)
Section titled “i18n.setDefaultNamespace(namespace)”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 everythingi18n.clearTranslations('de'); // Clear all German translationsi18n.clearTranslations('de', 'dashboard'); // Clear specific language + namespacei18n.reloadTranslations(language?, namespace?)
Section titled “i18n.reloadTranslations(language?, namespace?)”Reload translations from the registered loader.
await i18n.reloadTranslations(); // Reload current language + active namespacesawait i18n.reloadTranslations('de'); // Reload specific languageawait i18n.reloadTranslations('de', 'dashboard');i18n.addActiveNamespace(namespace)
Section titled “i18n.addActiveNamespace(namespace)”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 activei18n.addActiveNamespaces(namespaces)
Section titled “i18n.addActiveNamespaces(namespaces)”Load and activate multiple namespaces at once.
await i18n.addActiveNamespaces(['settings', 'billing']);i18n.getActiveNamespaces()
Section titled “i18n.getActiveNamespaces()”Returns the list of currently active namespaces.
const active = i18n.getActiveNamespaces(); // ['common', 'dashboard']i18n.getDefaultNamespace()
Section titled “i18n.getDefaultNamespace()”Returns the default namespace.
const defaultNs = i18n.getDefaultNamespace(); // 'common'i18n.getLoadedLocales()
Section titled “i18n.getLoadedLocales()”Returns all languages that have translations loaded in the cache.
const languages = i18n.getLoadedLocales(); // ['en', 'de']i18n.reportError(error, context?)
Section titled “i18n.reportError(error, context?)”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',});Events
Section titled “Events”i18n.on(event, callback)
Section titled “i18n.on(event, callback)”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();Available Events
Section titled “Available Events”| Event | Payload | Notes |
|---|---|---|
initialized | void | Instance finished init() |
destroyed | void | Instance 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 TMSi18n.on('missingKey', ({ key, locale, namespace }) => { reportMissingKey(key, locale, namespace);});
// Track loading state for UI spinnersi18n.on('loadingStateChanged', ({ isLoading }) => { showSpinner(isLoading);});
// Handle load errorsi18n.on('loadError', ({ locale, namespace, error }) => { console.error(`Failed to load ${locale}/${namespace}:`, error);});Plugin System
Section titled “Plugin System”Plugin Types
Section titled “Plugin Types”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().
Plugin-side APIs
Section titled “Plugin-side APIs”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 duringinit()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-processingreturn 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
Example Plugin
Section titled “Example Plugin”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.
Translation Cache
Section titled “Translation Cache”TranslationCache Class
Section titled “TranslationCache Class”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 detectionclone(): Map<string, FlattenedTranslations>; // 'locale:namespace' keysType-Safe Keys
Section titled “Type-Safe 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 tonever)
Framework packages re-export types; augmenting @comvi/core suffices for all bindings.
Virtual Nodes for Rich Text
Section titled “Virtual Nodes for Rich Text”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).
Global Access
Section titled “Global Access”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.
Important Gotchas
Section titled “Important Gotchas”i18n.locale = 'fr'is fire-and-forget — usesetLocaleAsync('fr')to await translation loading.params.fallbackchecked AFTERonMissingKey— missing key calls callbacks first, then tries fallback string.- Post-processors run FIFO — first registered runs first.
- Plugin cleanups run LIFO in
destroy(). - Import-map loader keys without
:expand to'locale:defaultNs'. - Apostrophes: inside word boundaries are literals; standalone
'is ICU quote delimiter. - Static template fast-path: no params + no post-procs + no special chars → string returned as-is.
setFallbackLocale(["en", "de"])accepts array (priority order) or single string.params.raw === trueis passed to post-processors. Post-processors that support it can opt out, for example the in-context editor marker injector.- Formatter caching:
Intl.*Formatcached perlocale + JSON.stringify(options). dirgetter: usesIntl.Locale.textInfo(ES2023+); falls back to hardcoded RTL list.