Vanilla JavaScript i18n
The @comvi/core package is the framework-agnostic foundation of Comvi. Use it directly when you don’t need a framework binding, or when building for environments like Web Components, static sites, or server-side scripts.
Installation
Section titled “Installation”pnpm add @comvi/core<script type="module"> import { createI18n } from 'https://esm.sh/@comvi/core';</script>-
Create the i18n instance with static translations
src/i18n.ts import { createI18n } from '@comvi/core';const i18n = createI18n({locale: 'en',fallbackLocale: 'en',translation: {en: {'hello.world': 'Hello, World!','welcome': 'Welcome, {name}!','nav.home': 'Home','nav.about': 'About',},de: {'hello.world': 'Hallo, Welt!','welcome': 'Willkommen, {name}!','nav.home': 'Startseite','nav.about': 'Uber uns',},},});await i18n.init(); -
Use the
t()functioni18n.t('hello.world'); // "Hello, World!"i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
The t() Function
Section titled “The t() Function”The t() function translates a key and optionally interpolates parameters.
i18n.t('hello.world'); // "Hello, World!"i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"i18n.t('page.title', { ns: 'dashboard' }); // Use a specific namespacei18n.t('greeting', { locale: 'de' }); // Force a specific languagei18n.t('new.key', { fallback: 'Fallback' }); // Fallback for missing keysSignature:
t(key: string, params?: TranslationParams): string| Parameter | Type | Description |
|---|---|---|
key | string | null | Dot-notation translation key. null returns "" |
params | object | Optional interpolation values plus the special options ns, locale, fallback, and raw |
See Translation Function for the full reference.
Language Switching
Section titled “Language Switching”Read or write the locale property to change the active locale. The setter is fire-and-forget — it kicks off namespace loading for the new locale in the background and any failure surfaces via the loadError event:
console.log(i18n.locale); // "en"
i18n.locale = 'de';
console.log(i18n.t('hello.world')); // "Hallo, Welt!"When you need to wait for the new locale’s translations before re-rendering, use setLocaleAsync() instead — it loads the active namespaces first and is safe to call rapidly (only the latest call wins):
await i18n.setLocaleAsync('de');// Translations for "de" are loaded; the UI can re-render nowconsole.log(i18n.t('hello.world')); // "Hallo, Welt!"Event-Based Reactivity
Section titled “Event-Based Reactivity”Since there is no framework reactivity system, Comvi uses events to notify your code when state changes. Subscribe to events and update your UI manually.
localeChanged
Section titled “localeChanged”Fires after the language changes and new translations are ready:
i18n.on('localeChanged', ({ from, to }) => { console.log(`Language changed from ${from} to ${to}`); updateUI();});namespaceLoaded
Section titled “namespaceLoaded”Fires when translations finish loading for a namespace:
i18n.on('namespaceLoaded', ({ locale, namespace }) => { console.log(`Translations loaded for: ${locale}/${namespace}`);});missingKey
Section titled “missingKey”Fires when t() is called with a key that does not exist:
i18n.on('missingKey', ({ key, locale, namespace }) => { console.warn(`Missing translation: "${key}" [${locale}/${namespace}]`);});Unsubscribing
Section titled “Unsubscribing”Every on() call returns an unsubscribe function:
const unsubscribe = i18n.on('localeChanged', updateUI);
// Later, stop listeningunsubscribe();DOM Updates
Section titled “DOM Updates”Without a framework, you update the DOM yourself in event handlers. Here is a practical pattern using data-i18n attributes and textContent:
<h1 data-i18n="hello.world"></h1><p data-i18n="welcome" data-i18n-params='{"name":"Alice"}'></p>
<select id="language-switcher"> <option value="en">English</option> <option value="de">Deutsch</option></select>import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', translation: { en: { 'hello.world': 'Hello, World!', 'welcome': 'Welcome, {name}!', }, de: { 'hello.world': 'Hallo, Welt!', 'welcome': 'Willkommen, {name}!', }, },});
function translateDOM() { document.querySelectorAll('[data-i18n]').forEach((el) => { const key = el.getAttribute('data-i18n')!; const paramsAttr = el.getAttribute('data-i18n-params'); const params = paramsAttr ? JSON.parse(paramsAttr) : undefined; el.textContent = i18n.t(key, params); });
document.documentElement.lang = i18n.locale;}
// Update DOM on language changei18n.on('localeChanged', translateDOM);
// Wire up the language switcherdocument.getElementById('language-switcher')!.addEventListener('change', (e) => { i18n.locale = (e.target as HTMLSelectElement).value;});
// Initial renderawait i18n.init();translateDOM();Plugin Integration
Section titled “Plugin Integration”Fetch Loader
Section titled “Fetch Loader”Load translations from the Comvi CDN or any HTTP endpoint instead of bundling them:
import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';
const i18n = createI18n({ locale: 'en', fallbackLocale: 'en',}) .use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', }));
await i18n.init();See Fetch Loader Plugin for all configuration options.
Language Detector
Section titled “Language Detector”Automatically detect the user’s preferred language from the browser, URL, or cookies:
import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';import { LocaleDetector } from '@comvi/plugin-locale-detector';
const i18n = createI18n({ locale: 'en',}) .use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', })) .use(LocaleDetector({ order: ['querystring', 'cookie', 'navigator'], caches: ['cookie'], }));Chaining Plugins
Section titled “Chaining Plugins”Plugins are chained with .use(). The order determines priority for detectors and the registration order for loaders:
const i18n = createI18n({ locale: 'en' }) .use(FetchLoader({ cdnUrl: '...' })) .use(LocaleDetector({ order: ['cookie', 'navigator'] }));TypeScript Support
Section titled “TypeScript Support”Type-Safe Keys via Module Augmentation
Section titled “Type-Safe Keys via Module Augmentation”Comvi i18n exposes a TranslationKeys interface from @comvi/core that you augment with your real keys. Once augmented, t() autocompletes keys and validates parameters at compile time:
import { createI18n } from '@comvi/core';
declare module '@comvi/core' { interface TranslationKeys { 'hello.world': never; // no params 'welcome': { name: string }; // required param 'nav.home': never; 'nav.about': never; }}
const i18n = createI18n({ locale: 'en',});
i18n.t('hello.world'); // OK — no paramsi18n.t('welcome', { name: 'Alice' }); // OKi18n.t('nonexistent.key'); // Type errori18n.t('welcome'); // Type error: missing 'name'Generated Types
Section titled “Generated Types”For larger projects, generate the augmentation file from your translations using the Comvi CLI. The CLI reads .comvirc.json and emits a .d.ts that re-opens @comvi/core with all your keys typed:
npx comvi typegenimport { createI18n } from '@comvi/core';// Side-effect import — augments @comvi/core's TranslationKeys interfaceimport './types/i18n';
const i18n = createI18n({ locale: 'en',});See Type-Safe Translations for the full setup, including CLI configuration and CI integration.
Web Components
Section titled “Web Components”Comvi works inside Web Components. Use the events API to update shadow DOM content:
import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en', translation: { en: { 'greeting': 'Hello from the shadow!' }, de: { 'greeting': 'Hallo aus dem Schatten!' }, },});
class MyWidget extends HTMLElement { private unsubscribe?: () => void;
connectedCallback() { this.attachShadow({ mode: 'open' }); this.render();
this.unsubscribe = i18n.on('localeChanged', () => { this.render(); });
i18n.init(); }
disconnectedCallback() { this.unsubscribe?.(); }
private render() { const p = document.createElement('p'); p.textContent = i18n.t('greeting');
const shadow = this.shadowRoot!; shadow.replaceChildren(p); }}
customElements.define('my-widget', MyWidget);Core Instance API
Section titled “Core Instance API”The most commonly used members of the I18n instance. This is the same surface every framework binding builds on. See the Core API Reference for the complete, method-by-method documentation — including the translation cache, the plugin-side hooks, and the window.__COMVI__ registry.
| Member | Type | Description |
|---|---|---|
t(key, params?) | (key, params?) => string | Translate a key to plain text |
tRaw(key, params?) | (key, params?) => TranslationResult | Return structured output for advanced renderers |
locale | string | Get or set the current locale. The setter is fire-and-forget — use setLocaleAsync() to await loading |
isLoading | boolean | Whether translations are currently being loaded |
isInitializing | boolean | Whether init() is currently running |
isInitialized | boolean | Whether init() has completed successfully |
dir | 'ltr' | 'rtl' | Text direction for the current locale |
init() | () => Promise<this> | Initialize the instance, run plugins, and load initial translations. Resolves to the instance (chainable) |
destroy() | () => Promise<void> | Tear down the instance: unregister from window.__COMVI__, run plugin cleanups (LIFO), clear caches and listeners. Use it for SPA route teardown and tests |
use(plugin, options?) | (plugin, options?) => this | Register a plugin (chainable) |
setLocaleAsync(locale) | (locale) => Promise<void> | Switch locale and wait for translations to load (race-safe) |
on(event, handler) | (event, handler) => () => void | Subscribe to an event (returns an unsubscribe function) |
addTranslations(translations) | (Record<string, Record<string, TranslationValue>>) => void | Merge translations at runtime, keyed by 'locale' or 'locale:namespace' |
hasTranslation(key, locale?, ns?, checkFallbacks?) | (...) => boolean | Check if a translation key exists in cache (no lookup) |
reloadTranslations(locale?, ns?) | (locale?, ns?) => Promise<void> | Reload translations from the registered loader |
addActiveNamespace(ns) / addActiveNamespaces(ns[]) | (...) => Promise<void> | Load and activate one or more namespaces |
getActiveNamespaces() / getDefaultNamespace() / getLoadedLocales() | () => string[] / () => string / () => string[] | Inspect namespace and locale state |
registerLoader(loader) / registerLocaleDetector(fn) / registerPostProcessor(fn) | (...) => void | Plugin-side hooks (usually called from inside a plugin) |
onMissingKey(cb) / onLoadError(cb) | (cb) => () => void | Subscribe to missing keys / load errors (returns an unsubscribe function) |
reportError(error, context?) | (...) => void | Report an error to the configured onError handler |