Migrate from i18next
t(), namespaces and the JSON-per-namespace file layout carry over from i18next; the instance
and the message syntax do not. i18next mutates a singleton through its own .use(), while
Comvi builds an i18n instance and composes capabilities onto it with .use(installer), and
Comvi messages are ICU, so every {{name}} and every _one / _other key pair gets touched.
Key differences
Section titled “Key differences”| Feature | i18next | Comvi |
|---|---|---|
| Instance | mutable singleton, i18next.use(…).init() |
createI18n({ … }) returning an instance you compose with .use(…) |
| Translation function | t('key') |
t('key'), same |
| Switching locale | i18next.changeLanguage('de') |
i18n.setLocale('de'), which awaits the namespace load |
| Interpolation | {{name}} (double curly) |
{name} (single curly, ICU standard) |
| Pluralization | suffix keys key_one / key_other |
one key, ICU {count, plural, …}; needs compiler: icuCompiler |
| Context | suffix keys key_male / key_female |
ICU {gender, select, …} |
| Nesting | $t(key) inside a value |
not supported; pass a parameter or make two t() calls |
| Namespaces | ns option, t('ns:key') or { ns } |
ns option and { ns }; a ns:key prefix is not parsed |
| Backend plugin | i18next-http-backend |
@comvi/plugin-fetch-loader |
| Language detection | i18next-browser-languagedetector |
@comvi/plugin-locale-detector |
| React binding | react-i18next (useTranslation, Trans) |
@comvi/react (useI18n, <T>) |
| Vue binding | i18next-vue |
@comvi/vue (useI18n, <T>) |
Migration steps
Section titled “Migration steps”-
Install the packages
Terminal npm install @comvi/react @comvi/plugin-fetch-loader @comvi/plugin-locale-detectornpm uninstall i18next react-i18next i18next-http-backend i18next-browser-languagedetectorFramework Install Uninstall React @comvi/react @comvi/plugin-fetch-loader @comvi/plugin-locale-detectori18next react-i18next i18next-http-backend i18next-browser-languagedetectorNext.js @comvi/next @comvi/plugin-fetch-loadersame as React Vue @comvi/vue @comvi/plugin-fetch-loader @comvi/plugin-locale-detectori18next i18next-vue i18next-http-backend i18next-browser-languagedetectorNuxt @comvi/nuxt @comvi/plugin-fetch-loadersame as Vue Svelte @comvi/svelte @comvi/plugin-fetch-loader @comvi/plugin-locale-detectori18next i18next-http-backend i18next-browser-languagedetectorSolid @comvi/solid @comvi/plugin-fetch-loader @comvi/plugin-locale-detectorsame as Svelte Vanilla @comvi/core @comvi/plugin-fetch-loader @comvi/plugin-locale-detectorsame as Svelte -
Replace the initialization
src/i18n.before.ts import i18next from 'i18next';import { initReactI18next } from 'react-i18next';import HttpBackend from 'i18next-http-backend';import LocaleDetector from 'i18next-browser-languagedetector';i18next.use(HttpBackend).use(LocaleDetector).use(initReactI18next).init({fallbackLng: 'en',ns: ['common', 'dashboard'],defaultNS: 'common',backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },detection: { order: ['querystring', 'cookie', 'navigator'], caches: ['cookie'] },});src/i18n.ts import { createI18n, icuCompiler } from '@comvi/react';import { cdnLoader } from '@comvi/plugin-fetch-loader';import { localeDetector } from '@comvi/plugin-locale-detector';export const i18n = createI18n({locale: 'en',fallbackLocale: 'en',ns: ['common', 'dashboard'],defaultNs: 'common',compiler: icuCompiler,}).use(localeDetector({supportedLocales: ['en', 'uk'], // without it a detected tag is truncated to its base languageorder: ['querystring', 'cookie', 'navigator'],caches: ['cookie'],lookupCookie: 'i18next', // the cookie name i18next wrotecacheFirst: false,}),).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));await i18n.init();createI18nfrom@comvi/reactis core’s constructor. Wrap your tree in<I18nProvider i18n={i18n}>in step 5.src/i18n/config.ts import { createNextI18n, icuCompiler } from '@comvi/next';import { cdnLoader } from '@comvi/plugin-fetch-loader';// `.use()` takes the installer. `cdnLoaderPlugin` is the raw plugin for// `registerPlugin(…)` and throws `E_USE_NOT_INSTALLER` here.export const { i18n, routing } = createNextI18n({locales: ['en', 'de'],defaultLocale: 'en',fallbackLocale: 'en',ns: ['common', 'dashboard'],defaultNs: 'common',compiler: icuCompiler,}).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));createNextI18nreturns an instance with the loader and plugin capabilities, the tag grammar and discovery in place; the CDN loader is composed with.use(), and ICU is thecompileroption, as on every constructor. Thennext.config.tsexports throughwithComvi({ config: './src/i18n/config.ts' })from@comvi/next/plugin, which points the server helpers at this module; without itgetI18n()throws in every Server Component. There is no locale detector: Next resolves the locale from the URL throughcreateMiddleware(routing). See Next.js.The React file with the imports from
@comvi/vueand noawait i18n.init()in it.createI18nbuilds aVueI18nwrapper, and.use()on the wrapper composes onto the instance it holds. Inmain.ts,app.use(i18n)startsinit()fire-and-forget when the core is not initialized, and anawait i18n.init()you add joins the attempt already running. Keep the.use()on the wrapper:i18n.core.use(…)runs but leavesi18n.coretyped as the base instance, so capability members do not type-check.nuxt.config.ts export default defineNuxtConfig({modules: ['@comvi/nuxt'],comvi: {locales: ['en', 'de'],defaultLocale: 'en',defaultNs: 'common',},});comvi.i18n.ts import { createI18n, defineComviI18n, icuCompiler } from '@comvi/nuxt/i18n';import { cdnLoader } from '@comvi/plugin-fetch-loader';export default defineComviI18n((options) =>createI18n({ ...options, compiler: icuCompiler }).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }),),);The module generates the i18n instance, and without a
comvi.i18n.tsfile next tonuxt.config.tsit is the base one: text and{param}only. The factory returns a new instance on every call.@comvi/nuxthas no CDN option, so the loader is composed incomvi.i18n.ts. The factory receives Nuxt’s resolved options, sonuxt.configstill governs locale, fallback and namespaces. Configure locale detection throughcomvi.localeDetectionand the cookie throughcomvi.localeCookie, not through@comvi/plugin-locale-detector.The React file with the imports from
@comvi/svelteand noawait i18n.init():setI18nContext(i18n)in a root component runsinit()for you, in step 5.The React file with the imports from
@comvi/solid.useI18n()reads Solid context, so provide the instance above every component that calls it, in step 5.The React file, with
createI18nimported from@comvi/coreandicuCompilerfrom@comvi/core/icu. Everything else is identical.One i18n instance, one
.use(installer)per capability:localeDetectorbrings the plugin capability,cdnLoaderbrings the loader and the plugin capability. Withcaches: ['cookie'], Comvi’s detector defaults tocacheFirst: true, which reads the first cache target before it walksorder, so a cookie from a previous visit beats the?lng=query parameter.cacheFirst: falseletsordergovern priority, as i18next’sdetectiondid; leave the default when a persisted choice should win over everything. Comvi’s cookie isi18n_langby default, solookupCookie: 'i18next'keeps the choice returning visitors already made.The order of the
.use(…)calls does not matter: the detector runs insideinit(), after every plugin and before the initial namespaces load, so the loader always sees the detected locale. -
Update translation calls
src/greet.ts import { i18n } from './i18n';// i18next catalog: "Hello, {{name}}!" → Comvi catalog: "Hello, {name}!"i18n.t('welcome', { name: 'Alice' });// Namespaced lookups are identical in both libraries.i18n.t('overview', { ns: 'dashboard' });t()is unchanged; the interpolation braces are not. The i18next prefix formt('dashboard:overview')is not parsed: it renders the key as is and fails type checking, so rewrite it with{ ns: 'dashboard' }. -
Replace the plugins
i18next plugin Comvi i18next-http-backend@comvi/plugin-fetch-loader(cdnLoader;fetchLoaderfrom/api)i18next-http-backendon your own files.use(attachLoader), theni18n.registerLoader((locale, ns) => fetch(…).then((r) => r.json()))i18next-browser-languagedetector@comvi/plugin-locale-detector(localeDetector)i18next-localstorage-backendno equivalent; bundle offline catalogs through the loader’s fallbacki18next-chained-backendnot needed; fallbackon the loader covers the offline pathinitReactI18nextnot needed; the binding reads the instance you pass to its provider Each first-party package exports an installer for
.use(…)and aPlugin-suffixed factory forregisterPlugin(…)on an instance that already has the plugin capability. Writing your own is on Write a plugin. -
Update the framework binding
src/MyComponent.before.tsx // Before (react-i18next)import { useTranslation, Trans } from 'react-i18next';export function MyComponent() {const { t } = useTranslation();return (<><h1>{t('title')}</h1><Trans i18nKey="legal.tos">Read our <a href="/terms">terms of service</a></Trans></>);}src/MyComponent.tsx // After (Comvi), catalog: "Read our <link>terms of service</link>"import { useI18n, T } from '@comvi/react';export function MyComponent() {const { t } = useI18n();return (<><h1>{t('title')}</h1><T i18nKey="legal.tos" components={{ link: <a href="/terms" /> }} /></>);}useTranslation()becomesuseI18n().<Trans>took the fallback markup from its children;<T>takes the tags from the message and maps them throughcomponents, so the English copy moves into the catalog. Put<I18nProvider i18n={i18n}>above the tree. Per framework:- Next.js:
useI18nfrom@comvi/next/clientin'use client'components, andconst { t } = await getI18n({ locale })from@comvi/next/serverin a Server Component.getI18n()reads the instance from the modulewithComvinames. The[locale]layout renders<ComviProvider locale>from@comvi/next/server. See Next.js. - Vue:
useTranslation()fromi18next-vuebecomesuseI18n()from@comvi/vue. - Nuxt: register the module in
nuxt.config.tsas in step 2;useI18nis auto-imported, so no import line at all. - Svelte: a root component calls
setI18nContext(i18n), thenuseI18n()in any descendant.tis a store, so the markup reads{$t('title')}.useI18n()is callable during component initialisation only and throws when no ancestor calledsetI18nContext. - Solid: wrap the tree in
<I18nProvider i18n={i18n}>from@comvi/solid;useI18n()reads context and throws without it. Nothing about Comvi is global. - Vanilla:
i18next.t('title')becomesi18n.t('title')on the instance you exported.
- Next.js:
-
Migrate the translation files
The layout carries over: one JSON per namespace per locale, nested keys flattened with dots.
Value i18next Comvi Interpolation "Hello, {{name}}!""Hello, {name}!"Plurals items_zero,items_one,items_otherkeys"items": "{count, plural, =0 {No items} one {# item} other {# items}}"Context key_male/key_femalekeys"{gender, select, male {He} female {She} other {They}} left a review"Nesting "Welcome! $t(greeting)""Welcome! {greeting}", called ast('welcome', { greeting: t('greeting') })<Trans>tags"Read our <1>terms</1>"(child index)"Read our <link>terms</link>"; named tags from<Trans components>carry overThe brace change is a search and replace. The plural change is not: three suffix keys collapse into one message, the count moves inside it as
#, and every call site that passedcountkeeps working. A context call changes:{ context: 'female' }becomes the select’s own parameter,{ gender: 'female' }. Then import each locale’s JSON in your project on app.comvi.io. -
Turn on the type checking
src/types.ts import './types/i18n';npx comvi typegenfetches the schema from the Comvi platform and writes a.d.ts(comvi typegen);comviTypes({ translations })invite.config.tsregenerates from local JSON on every change, and reads the i18next folder layout withfileTemplate: '{languageTag}/{namespace}.json'anddefaultNs: 'common'(Vite plugin). Both use the same emitter, so CI can run one and dev the other. Import the generated file once, for its side effect. From then on an unknown key, a missing parameter and a wrong parameter type are compile errors in every binding. Addcomvi typegen --checkto CI. -
Remove i18next
Terminal grep -r "i18next" src/ --include="*.ts" --include="*.tsx" --include="*.vue"Then drop every
i18next*package, anyi18next.config.js/.i18nrc, and thepackage.jsonscripts that called i18next CLI tools.
Syntax comparison reference
Section titled “Syntax comparison reference”| Pattern | i18next | Comvi |
|---|---|---|
| Interpolation | Hello, {{name}}! |
Hello, {name}! |
| Plural | key_one / key_other keys |
{count, plural, one {# item} other {# items}} |
| Ordinal | key_ordinal_one keys |
{pos, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} |
| Context | key_male / key_female keys |
{gender, select, male {…} female {…} other {…}} |
| Nesting | $t(otherKey) |
pass a parameter, or make two t() calls |
| Number format | {{val, number}} |
formatNumber(val) from useI18n(), or formatNumber(i18n, val) from @comvi/core; {val} in the message |
| Date format | {{val, datetime}} |
formatDate(val, options) from useI18n(), or formatDate(i18n, val, options); {val} in the message |
| Escaping a brace | not needed | wrap it in single quotes: '{' |
| Rich text | <Trans> with children as source |
<T> with the tags in the message and components for the elements |
API mapping reference
Section titled “API mapping reference”| i18next | Comvi |
|---|---|
i18next.changeLanguage(lng) |
i18n.setLocale(locale), a promise that resolves after the load |
i18next.language |
i18n.locale |
t(key, { defaultValue: 'Hi' }) |
t(key, { fallback: 'Hi' }) |
i18next.exists(key) |
i18n.hasTranslation(key). On @comvi/vue / @comvi/nuxt that returns a ComputedRef and is therefore always truthy in an if, so the boolean form is hasTranslationNow(key) on the instance (inject(I18N_INJECTION_KEY)! in Vue, useNuxtApp().$i18n in Nuxt), not on useI18n() |
i18next.on('languageChanged', fn) |
i18n.on('localeChanged', fn), payload { from, to } |
useTranslation('dashboard') |
useI18n('dashboard'), which binds t but loads nothing: list the namespace in ns, or call addActiveNamespace('dashboard') from useI18nLoader() |
missingKeyHandler (with saveMissing) |
i18n.on('missingKey', fn), payload { key, locale, namespace } |
parseMissingKeyHandler |
the onMissingKey constructor option, or useI18nPlugins() at runtime; its return value replaces the key |
i18next.use(Backend) |
.use(cdnLoader({ … })), or .use(fetchLoader({ … })) from /api |
i18next.use(LanguageDetector) |
.use(localeDetector({ … })) |
fallback applies once the whole locale chain has missed and then outranks every
onMissingKey handler: plugin callbacks still run to record the miss, but their result is
discarded and the constructor option is never consulted. The string is interpolated with the
same params, so t('greeting', { name: 'Alice', fallback: 'Hi, {name}!' }) renders Hi, Alice!.
Migration checklist
Section titled “Migration checklist”- Install the binding,
@comvi/plugin-fetch-loader, and@comvi/plugin-locale-detectorif you used the i18next detector - Replace
i18next.use(…).init()with your framework’s constructor plus one.use(installer)per capability - Set
cacheFirst: falseon the detector iforderis meant to outrank a persisted choice, andlookupCookie: 'i18next'to keep the cookie i18next wrote - Decide the compiler:
compiler: icuCompiler, or nothing if you use no plurals - Update
useTranslation()touseI18n(), and everyt('ns:key')tot('key', { ns }) - Replace
changeLanguage(lng)withsetLocale(locale), andawaitit: the barei18n.locale = 'de'setter is fire-and-forget and reports failure as aloadErrorevent instead of a rejection - Rename
defaultValuetofallback,exists()tohasTranslation()(on Vue and Nuxt,hasTranslationNow()on the instance), andlanguageChangedtolocaleChanged - Wrap the tree in the binding’s provider where it has one
- Replace
<Trans>with<T>, moving the copy from children into the catalog - In every value:
{{name}}to{name}, suffix keys to ICUplural/select,{{val, number}}/{{val, datetime}}to a formatter call in code, no$t(key)nesting - Import the catalogs into the Comvi platform
- Generate the types with
comvi typegen, or withcomviTypes()invite.config.ts, and import the generated.d.tsonce - Uninstall every
i18next*package and its config files - Run the app and check that no key renders as its own name
Read next
Section titled “Read next”- How an i18n instance is built: what the base instance has, and what
.use(installer)adds. - Pluralize with ICU: which compiler you have, and exactly what ICU implements.
- Fetch loader: CDN mode, API mode, and offline fallbacks.