Skip to content

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.

The most common approach. The Fetch Loader plugin downloads translations from Comvi’s CDN at runtime:

src/i18n.ts
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

Bundle translations directly in your app. Good for offline-first apps or when you want zero network dependency:

src/i18n.ts
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:

src/i18n.ts
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.

Build your own loader for any data source — Firebase, a custom API, local files, etc.:

src/i18n.ts
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();
});
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.

src/i18n.ts
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.

0. createI18n({ ... }) — instance created; any static `translation`
option is populated into the cache immediately, so calls to t()
work before init() resolves
1. init() called — isLoading and isInitializing flip to true
2. Plugins initialize (fetch loader, locale detector, …)
3. Locale detector (if registered) picks the active locale
4. Initial namespaces are loaded for the active locale via the
registered loader. Defaults to [defaultNs] unless `ns: [...]` was
passed; pass `ns: []` to skip
5. Translations land in the cache
6. isLoading and isInitializing flip to false; components re-render

Handle the loading state in your UI:

function App() {
const { t, isLoading } = useI18n();
if (isLoading) return <Skeleton />;
return <h1>{t('page.title')}</h1>;
}

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 during init())
  • 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 own
const { 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.

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.

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" namespace
i18n.addTranslations({
'en': { 'new.key': 'New value' }, // → en:defaultNs
'en:admin': { 'dashboard.title': 'Admin' }, // → en:admin
});
// Check if a translation exists
i18n.hasTranslation('some.key'); // current locale + defaultNs only
i18n.hasTranslation('some.key', 'de'); // specific locale
i18n.hasTranslation('some.key', 'de', 'admin'); // specific locale + namespace
i18n.hasTranslation('some.key', undefined, undefined, true); // also walk fallbackLocale chain

The 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.

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.