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 | Dot-notation translation key |
params | object | Optional interpolation values, namespace, language, or default value |
See Translation Function for the full reference.
Language Switching
Section titled “Language Switching”Read or write the locale property to change the active locale:
console.log(i18n.locale); // "en"
i18n.locale = 'de';
console.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 { 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 properties and methods of the i18n instance. See the Core API Reference for the complete surface.
| Property / Method | 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 |
isLoading | boolean | Whether translations are being loaded |
isInitialized | boolean | Whether init() has completed |
init() | () => Promise<void> | Initialize the instance and load translations |
use(plugin) | (plugin) => i18n | Register a plugin (chainable) |
setLocaleAsync(lang) | (lang) => Promise<void> | Switch language and wait for translations to load |
on(event, handler) | (event, handler) => () => void | Subscribe to an event (returns unsubscribe function) |
addTranslations(translations) | (Record<string, Record<string, TranslationValue>>) => void | Add translations at runtime (keyed by language code) |
hasTranslation(key, lang?) | (key, lang?) => boolean | Check if a translation key exists |
reloadTranslations(lang?, ns?) | (lang?, ns?) => Promise<void> | Force reload translations from loader |
addActiveNamespace(ns) | (ns) => Promise<void> | Load a namespace dynamically |