Features
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.
Translate text with t()
Section titled “Translate text with t()”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.
import { useI18n } from '@comvi/react';
export function Greeting() { const { t } = useI18n(); return <h1>{t('welcome', { name: 'Alice' })}</h1>;}Guide: Translate text with t()
Insert values and markup
Section titled “Insert values and markup”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.
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
Pluralize with ICU
Section titled “Pluralize with ICU”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.
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 and currency
Section titled “Format numbers, dates and currency”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.
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
Section titled “Switch the locale”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.
import { createI18n } from '@comvi/core';
export const i18n = createI18n({ locale: 'de', fallbackLocale: 'en' });
i18n.setFallbackLocale(['de-AT', 'de', 'en']); // tried in orderawait i18n.setLocale('fr'); // resolves once the locale is appliedGuide: Switch the locale
Load translations on demand
Section titled “Load translations on demand”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.
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 translations into namespaces
Section titled “Split translations into namespaces”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.
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'), }),);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
Type-check translation keys
Section titled “Type-check translation keys”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.
import '@comvi/react';
declare module '@comvi/react' { interface TranslationKeys { "greeting": { "name": string }; "hello": never; }}
export {};Guide: Type-check translation keys
Render on the server
Section titled “Render on the server”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.
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
Handle missing keys and errors
Section titled “Handle missing keys and errors”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.
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)beforeinit(); 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.