Loading Translations
Comvi supports multiple strategies for loading translations into your app. Choose based on your requirements for freshness, performance, and offline support.
When you use @comvi/core directly, registered loaders run during await i18n.init() and during later calls such as setLocaleAsync() or addActiveNamespace(). Framework integrations usually initialize the instance for you through their provider/plugin setup.
Loading Strategies
Section titled “Loading Strategies”1. CDN Fetch (Recommended)
Section titled “1. CDN Fetch (Recommended)”The most common approach. The Fetch Loader plugin downloads translations from Comvi’s CDN at runtime:
import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';
const i18n = createI18n({ locale: 'en' }) .use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', }));Pros: Always up-to-date, no app redeploy needed, small bundle size, HTTP caching Cons: Requires network request on first load
2. Static Translations
Section titled “2. Static Translations”Bundle translations directly in your app. Good for offline-first apps or when you want zero network dependency:
import { createI18n } from '@comvi/core';import en from './locales/en.json';import de from './locales/de.json';
const i18n = createI18n({ locale: 'en', translation: { en, de, },});Pros: Instant availability, works offline, no network dependency Cons: Requires app redeploy to update translations, increases bundle size
To avoid this, use registerLoader with dynamic import() so each language is code-split into a separate chunk and loaded on demand:
import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en' });
i18n.registerLoader({ en: () => import('./locales/en.json'), de: () => import('./locales/de.json'), fr: () => import('./locales/fr.json'),});The bundler (Vite, webpack, etc.) splits each language into a separate chunk automatically. During init(), only the active locale and configured initial namespaces are loaded.
3. Custom Loaders
Section titled “3. Custom Loaders”Build your own loader for any data source — Firebase, a custom API, local files, etc.:
import { createI18n } from '@comvi/core';
const i18n = createI18n({ locale: 'en' });
i18n.registerLoader(async (locale, namespace) => { const response = await fetch(`/api/translations/${locale}/${namespace}`); return response.json();});4. CDN + Fallback Imports (Recommended for Production)
Section titled “4. CDN + Fallback Imports (Recommended for Production)”Combine CDN and static imports: the Fetch Loader tries the CDN first, then falls back to bundled translations if the network is unavailable. The fallback option ensures offline support while keeping the bundle code-split.
import { createI18n } from '@comvi/core';import { FetchLoader } from '@comvi/plugin-fetch-loader';
const i18n = createI18n({ locale: 'en' }) .use(FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id', fallback: { 'en': () => import('./locales/en.json'), 'de': () => import('./locales/de.json'), }, }));Key formats: 'lang' (default namespace) or 'lang:namespace'. See Fetch Loader › Fallback Imports for more details. This is the recommended setup for production apps.
Loading Lifecycle
Section titled “Loading Lifecycle”0. createI18n({ ... }) — instance created; any static `translation` option is populated into the cache immediately, so calls to t() work before init() resolves1. init() called — isLoading and isInitializing flip to true2. Plugins initialize (fetch loader, locale detector, …)3. Locale detector (if registered) picks the active locale4. Initial namespaces are loaded for the active locale via the registered loader. Defaults to [defaultNs] unless `ns: [...]` was passed; pass `ns: []` to skip5. Translations land in the cache6. isLoading and isInitializing flip to false; components re-renderLoading States
Section titled “Loading States”Handle the loading state in your UI:
function App() { const { t, isLoading } = useI18n();
if (isLoading) return <Skeleton />; return <h1>{t('page.title')}</h1>;}<template> <Skeleton v-if="isLoading" /> <h1 v-else>{{ t('page.title') }}</h1></template>'use client';
function App() { const { t, isLoading } = useI18n();
if (isLoading) return <Skeleton />; return <h1>{t('page.title')}</h1>;}<template> <Skeleton v-if="isLoading" /> <h1 v-else>{{ t('page.title') }}</h1></template>function App() { const { t, isLoading } = useI18n();
if (isLoading()) return <Skeleton />; return <h1>{t('page.title')}</h1>;}<script lang="ts">import { useI18n } from '@comvi/svelte';
const { t, isLoading } = useI18n();</script>
{#if $isLoading} <Skeleton />{:else} <h1>{$t('page.title')}</h1>{/if}import { i18n } from './i18n';
const render = () => { if (i18n.isLoading) { renderSkeleton(); } else { document.querySelector('h1')!.textContent = i18n.t('page.title'); }};
i18n.on('loadingStateChanged', render);i18n.on('localeChanged', render);render();Namespace Loading
Section titled “Namespace Loading”Calling useI18n(ns) only scopes subsequent t() calls to that namespace — it does not load it. To make a namespace available, you have to register it explicitly through one of:
- the
ns: [...]option at construction (loaded eagerly duringinit()) - the
translation: { 'en:dashboard': ... }option for static bundles - a runtime call to
i18n.addActiveNamespace('dashboard')— typical from a router guard or route loader
// Scopes t() to "dashboard", but does NOT trigger a fetch on its ownconst { t: tDash } = useI18n('dashboard');
// Make sure the namespace is loaded before/around the component mount:await i18n.addActiveNamespace('dashboard');See Namespaces › Lazy Loading on Route Change for the route-loader patterns.
Caching
Section titled “Caching”Comvi i18n caches loaded translations in memory by (locale, namespace). Subsequent lookups for the same pair are direct cache reads.
The Fetch Loader plugin also respects HTTP cache headers from the CDN, providing browser-level caching.
Runtime Translation Management
Section titled “Runtime Translation Management”Add or override individual translations at runtime. addTranslations is a merge — keys present in the new object override existing keys for that locale:namespace, and untouched keys stay as they were:
// Merge translations into the default namespace and "admin" namespacei18n.addTranslations({ 'en': { 'new.key': 'New value' }, // → en:defaultNs 'en:admin': { 'dashboard.title': 'Admin' }, // → en:admin});
// Check if a translation existsi18n.hasTranslation('some.key'); // current locale + defaultNs onlyi18n.hasTranslation('some.key', 'de'); // specific localei18n.hasTranslation('some.key', 'de', 'admin'); // specific locale + namespacei18n.hasTranslation('some.key', undefined, undefined, true); // also walk fallbackLocale chainThe 4th argument toggles fallback-chain lookup; it defaults to false, so by default hasTranslation only inspects the exact (locale, namespace) you pass. If you want “is this key resolvable for the current user”, pass true.
Deduplication
Section titled “Deduplication”Concurrent requests for the same (locale, namespace) pair are deduplicated at the core level, regardless of which loader is registered. If two parts of the app request en:dashboard at the same time, the loader function is called once and every caller awaits the same pending Promise.
The in-memory cache has a revision counter that framework bindings use for fast change detection.