Skip to content
Comvi

Migrate from i18next

View .md

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.

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>)
  1. Install the packages

    Terminal
    npm install @comvi/react @comvi/plugin-fetch-loader @comvi/plugin-locale-detector
    npm uninstall i18next react-i18next i18next-http-backend i18next-browser-languagedetector
    Framework Install Uninstall
    React @comvi/react @comvi/plugin-fetch-loader @comvi/plugin-locale-detector i18next react-i18next i18next-http-backend i18next-browser-languagedetector
    Next.js @comvi/next @comvi/plugin-fetch-loader same as React
    Vue @comvi/vue @comvi/plugin-fetch-loader @comvi/plugin-locale-detector i18next i18next-vue i18next-http-backend i18next-browser-languagedetector
    Nuxt @comvi/nuxt @comvi/plugin-fetch-loader same as Vue
    Svelte @comvi/svelte @comvi/plugin-fetch-loader @comvi/plugin-locale-detector i18next i18next-http-backend i18next-browser-languagedetector
    Solid @comvi/solid @comvi/plugin-fetch-loader @comvi/plugin-locale-detector same as Svelte
    Vanilla @comvi/core @comvi/plugin-fetch-loader @comvi/plugin-locale-detector same as Svelte
  2. 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 language
    order: ['querystring', 'cookie', 'navigator'],
    caches: ['cookie'],
    lookupCookie: 'i18next', // the cookie name i18next wrote
    cacheFirst: false,
    }),
    )
    .use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));
    await i18n.init();

    createI18n from @comvi/react is core’s constructor. Wrap your tree in <I18nProvider i18n={i18n}> in step 5.

    One i18n instance, one .use(installer) per capability: localeDetector brings the plugin capability, cdnLoader brings the loader and the plugin capability. With caches: ['cookie'], Comvi’s detector defaults to cacheFirst: true, which reads the first cache target before it walks order, so a cookie from a previous visit beats the ?lng= query parameter. cacheFirst: false lets order govern priority, as i18next’s detection did; leave the default when a persisted choice should win over everything. Comvi’s cookie is i18n_lang by default, so lookupCookie: 'i18next' keeps the choice returning visitors already made.

    The order of the .use(…) calls does not matter: the detector runs inside init(), after every plugin and before the initial namespaces load, so the loader always sees the detected locale.

  3. 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 form t('dashboard:overview') is not parsed: it renders the key as is and fails type checking, so rewrite it with { ns: 'dashboard' }.

  4. Replace the plugins

    i18next plugin Comvi
    i18next-http-backend @comvi/plugin-fetch-loader (cdnLoader; fetchLoader from /api)
    i18next-http-backend on your own files .use(attachLoader), then i18n.registerLoader((locale, ns) => fetch(…).then((r) => r.json()))
    i18next-browser-languagedetector @comvi/plugin-locale-detector (localeDetector)
    i18next-localstorage-backend no equivalent; bundle offline catalogs through the loader’s fallback
    i18next-chained-backend not needed; fallback on the loader covers the offline path
    initReactI18next not needed; the binding reads the instance you pass to its provider

    Each first-party package exports an installer for .use(…) and a Plugin-suffixed factory for registerPlugin(…) on an instance that already has the plugin capability. Writing your own is on Write a plugin.

  5. 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() becomes useI18n(). <Trans> took the fallback markup from its children; <T> takes the tags from the message and maps them through components, so the English copy moves into the catalog. Put <I18nProvider i18n={i18n}> above the tree. Per framework:

    • Next.js: useI18n from @comvi/next/client in 'use client' components, and const { t } = await getI18n({ locale }) from @comvi/next/server in a Server Component. getI18n() reads the instance from the module withComvi names. The [locale] layout renders <ComviProvider locale> from @comvi/next/server. See Next.js.
    • Vue: useTranslation() from i18next-vue becomes useI18n() from @comvi/vue.
    • Nuxt: register the module in nuxt.config.ts as in step 2; useI18n is auto-imported, so no import line at all.
    • Svelte: a root component calls setI18nContext(i18n), then useI18n() in any descendant. t is a store, so the markup reads {$t('title')}. useI18n() is callable during component initialisation only and throws when no ancestor called setI18nContext.
    • 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') becomes i18n.t('title') on the instance you exported.
  6. 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_other keys "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
    Context key_male / key_female keys "{gender, select, male {He} female {She} other {They}} left a review"
    Nesting "Welcome! $t(greeting)" "Welcome! {greeting}", called as t('welcome', { greeting: t('greeting') })
    <Trans> tags "Read our <1>terms</1>" (child index) "Read our <link>terms</link>"; named tags from <Trans components> carry over

    The 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 passed count keeps 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.

  7. Turn on the type checking

    src/types.ts
    import './types/i18n';

    npx comvi typegen fetches the schema from the Comvi platform and writes a .d.ts (comvi typegen); comviTypes({ translations }) in vite.config.ts regenerates from local JSON on every change, and reads the i18next folder layout with fileTemplate: '{languageTag}/{namespace}.json' and defaultNs: '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. Add comvi typegen --check to CI.

  8. Remove i18next

    Terminal
    grep -r "i18next" src/ --include="*.ts" --include="*.tsx" --include="*.vue"

    Then drop every i18next* package, any i18next.config.js / .i18nrc, and the package.json scripts that called i18next CLI tools.

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

  • Install the binding, @comvi/plugin-fetch-loader, and @comvi/plugin-locale-detector if you used the i18next detector
  • Replace i18next.use(…).init() with your framework’s constructor plus one .use(installer) per capability
  • Set cacheFirst: false on the detector if order is meant to outrank a persisted choice, and lookupCookie: 'i18next' to keep the cookie i18next wrote
  • Decide the compiler: compiler: icuCompiler, or nothing if you use no plurals
  • Update useTranslation() to useI18n(), and every t('ns:key') to t('key', { ns })
  • Replace changeLanguage(lng) with setLocale(locale), and await it: the bare i18n.locale = 'de' setter is fire-and-forget and reports failure as a loadError event instead of a rejection
  • Rename defaultValue to fallback, exists() to hasTranslation() (on Vue and Nuxt, hasTranslationNow() on the instance), and languageChanged to localeChanged
  • 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 ICU plural / 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 with comviTypes() in vite.config.ts, and import the generated .d.ts once
  • Uninstall every i18next* package and its config files
  • Run the app and check that no key renders as its own name