Skip to content
Comvi

Features

View .md

What the library does, one section per capability with the shortest snippet that shows it. The i18n instance and t() are the same in every framework; the snippets use React where a component is needed.

Call t() with a key and get the text for the current locale. Keys are dot paths, so a nested catalog and a flat one with dotted keys are the same catalog to the instance, and { ns } in the params reads another namespace.

src/Greeting.tsx
import { useI18n } from '@comvi/react';
export function Greeting() {
const { t } = useI18n();
return <h1>{t('welcome', { name: 'Alice' })}</h1>;
}

Guide: Translate text with t()

Fill {param} placeholders with t() and render tags such as <link> inside a translation with <T>. <T> hands each tag to the component under its name, so the translator can move the tag to where the sentence needs it and your code still decides what it renders.

src/Help.tsx
import { T } from '@comvi/react';
// "help": "Click <link>here</link> for help"
export function Help() {
return <T i18nKey="help" components={{ link: <a href="/help" /> }} />;
}

Plain t() leaves tags in the string until the bundle imports @comvi/core/tags.

Guide: Insert values and markup

Write {count, plural, …} in a message and let t() pick the form the locale needs. compiler: icuCompiler adds plural, select and selectordinal to the default compiler, which handles text and {param} only. The option is set once on the instance and covers every catalog a loader or the CDN adds later.

src/i18n.ts
import { createI18n } from '@comvi/core';
import { icuCompiler } from '@comvi/core/icu';
export const i18n = createI18n({
locale: 'en',
compiler: icuCompiler,
messages: { en: { 'cart.items': '{count, plural, =0 {Your cart is empty} one {# item} other {# items}}' } },
});

count goes to Intl.PluralRules and # becomes the number; without the compiler, an ICU message throws at ingestion in development and renders literally in production.

Guide: Pluralize with ICU

Format numbers, dates, currency and relative time in the current locale with the Intl wrappers that useI18n() returns. In every binding each wrapper follows the current locale, so the output follows a locale switch on its own. The options argument forwards to the Intl constructor.

src/PriceTag.tsx
import { useI18n } from '@comvi/react';
export function PriceTag({ amount, at }: { amount: number; at: Date }) {
const { formatCurrency, formatDate } = useI18n();
return <span>{formatCurrency(amount, 'EUR')} · {formatDate(at, { dateStyle: 'long' })}</span>;
}

Outside a component, the root exports of @comvi/core take the instance as their first argument. Messages do not evaluate number or date skeletons: format the value first and pass the string as a parameter.

Guide: Format numbers, dates and currency

Switch the locale at runtime and every component that reads the instance re-renders. setLocale() emits localeChanged and resolves once the locale is applied; with a loader composed, it loads the new locale’s namespaces first, so the page never shows keys between the two. A key missing from the current locale is looked up along the fallbackLocale chain before missingKey fires.

src/switch.ts
import { createI18n } from '@comvi/core';
export const i18n = createI18n({ locale: 'de', fallbackLocale: 'en' });
i18n.setFallbackLocale(['de-AT', 'de', 'en']); // tried in order
await i18n.setLocale('fr'); // resolves once the locale is applied

Guide: Switch the locale

Load each locale when the user needs it instead of bundling every catalog. A loader is composed with .use() in the same expression that creates the instance. loader(map) turns each entry into a dynamic import(), so the bundler emits one chunk per locale and a locale is fetched only when it is active or in the fallback chain.

src/i18n.ts
import { createI18n } from '@comvi/core';
import { loader } from '@comvi/core/loader';
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en' }).use(
loader({
en: () => import('./locales/en.json'),
de: () => import('./locales/de.json'),
}),
);

init() loads the active locale and its fallback chain; setLocale() and reloadMessages() go through the same map later. The framework providers call init() for you.

Guide: Load translations on demand

Split the catalog into namespaces and load each one only on the screens that use it. A map key is locale for the default namespace or locale:namespace for one namespace of one locale; ns names the namespaces loaded during init(), and addActiveNamespace() loads another later.

src/i18n.ts
import { createI18n } from '@comvi/core';
import { loader } from '@comvi/core/loader';
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en', ns: ['default', 'dashboard'] }).use(
loader({
'en': () => import('./locales/en.json'),
'en:dashboard': () => import('./locales/dashboard/en.json'),
}),
);
src/Dashboard.tsx
import { useI18n } from '@comvi/react';
export function Dashboard() {
const { t } = useI18n('dashboard'); // every t() in here reads the dashboard namespace
return <h1>{t('page.title')}</h1>;
}

Guide: Split translations into namespaces

Generate a TranslationKeys declaration from your catalogs so a misspelled key, a missing parameter or a wrong parameter type is a TypeScript error in your editor. comviTypes() from @comvi/vite-plugin writes it from local JSON with no account; comvi typegen writes the same file from the key schema of a Comvi project. Import it once, in the module that builds the instance.

src/types/i18n.d.ts
import '@comvi/react';
declare module '@comvi/react' {
interface TranslationKeys {
"greeting": { "name": string };
"hello": never;
}
}
export {};

Guide: Type-check translation keys

Render translated text into the server response so crawlers, link previews and the first paint see real copy. One question decides the shape of every setup: may the instance live at module scope on the server? Next.js is the one framework where it may, because getI18n() binds the locale per request; every other framework builds an instance per request and hands the client a serializable messages payload.

src/lib/i18n.server.ts
import { createI18n } from '@comvi/core';
import { cdnLoader } from '@comvi/plugin-fetch-loader';
export async function loadI18n(locale: string) {
const i18n = createI18n({ locale, fallbackLocale: 'en' }).use(
cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }),
);
await i18n.init();
const namespace = i18n.getDefaultNamespace();
return { locale, messages: { [`${locale}:${namespace}`]: i18n.getMessages(locale, namespace) ?? {} } };
}

The client creates its instance from that payload as messages and passes autoInit: false to the provider, which is where the option lives: autoInit={false} on <I18nProvider>, or the second argument of setI18nContext() in Svelte. @comvi/next does the per-request work with <ComviProvider locale> and getI18n({ locale }), and the Nuxt module builds a fresh instance per request from comvi.i18n.ts.

Guide: Render on the server

Keep the app rendering when a key is missing or a catalog fails to load. t() never throws on a missing key, and every failure reaches a hook you can wire to your error tracker. The lookup walks the fallback chain, emits missingKey and returns whatever onMissingKey returns, or the key itself; onError receives every reported failure with a context.source naming where it came from.

src/i18n.ts
import { createI18n } from '@comvi/core';
declare function report(error: unknown, source?: string): void;
export const i18n = createI18n({
locale: 'de-AT',
fallbackLocale: ['de', 'en'],
onMissingKey: ({ key }) => (import.meta.env.DEV ? `[MISSING: ${key}]` : ''),
onError: (error, context) => report(error, context?.source),
});

onLoadError fires once per locale:namespace pair a loader failed to load.

Guide: Handle missing keys and errors

  • Installation: one package per framework; each binding depends on @comvi/core, so the core comes with it.
  • Plugins overview: an installer takes the i18n instance and returns it, composed with .use(installer) before init(); the fetch loader, the locale detector and the in-context editor are installers.
  • Connect to Comvi: replace the inline catalog with one the Comvi platform serves over its CDN, and ship a wording change without a redeploy.