Guides

Migrate from i18next

Both tools speak JSON, so this is easier than it looks: your keys and translations port almost directly, the plural suffixes merge into one message, and the runtime swap is mostly find-and-replace. You also drop ~20 KB of runtime on the way.

What maps to what

The mental model carries over one-to-one, and syntax is the only thing that changes:

i18nextVerbaly
i18next.init({ … })npx verbaly init + one build plugin
t('inbox.title')t('inbox.title'): same call, keys now typed
{{name}}{name}
messages_one / messages_other{count | one: … | other: # …}
format: 'currency' (interpolation){price:currency/EUR} (native Intl)
i18next-browser-languagedetectorresolveLocale() + persistLocale()
i18next-http-backend (lazy load)per-locale code-splitting, built into the plugin
useTranslation() (react-i18next)useT() (@verbaly/react)
Trans componentTrans component: values / components map over
i18next.changeLanguage('es')await setLocale('es')
saveMissing, runtime fallbacksnpx verbaly check: the build fails instead

Keep your keys: catalogs port almost directly

Verbaly catalogs are flat JSON with no proprietary format. Flatten nested groups into dotted keys, and merge the _one/_other plural suffixes into a single message with variants, and every existing translation survives:

locales/en.json (i18next)
{
  "inbox": { "title": "Inbox" },
  "messages_one": "one message",
  "messages_other": "{{count}} messages"
}
locales/en.json (Verbaly)
{
  "inbox.title": "Inbox",
  "messages": "{count | one: one message | other: # messages}"
}

Namespaces (ns:key) become part of the dotted key (ns.key). The # inside a variant renders the count already localized, with no separate formattedCount interpolation needed.

Swap the runtime

Existing keys keep working through t('key', params), so migrated components barely change. In React:

before (react-i18next)
import { useTranslation } from 'react-i18next';

const { t } = useTranslation();
t('inbox.title');
t('messages', { count });
after (@verbaly/react)
import { useT } from '@verbaly/react';

const t = useT();
t('inbox.title');
t('messages', { count });  // params now typed

New copy skips the key ceremony entirely: write the text as a tagged template and let the compiler extract it, or keep readable ids with t.id. Vue and Svelte adapters follow the same pattern, and plain-HTML pages can bind with data-verbaly instead.

src/app.tsx
t`Hola ${name}`;                    // key + types generated
t.id('inbox.title')`Inbox`;      // readable id, i18next-style

Locale detection and lazy loading

The detector plugin and the HTTP backend both disappear. resolveLocale reads storage → navigator.languages → fallback (SSR-safe, BCP-47 narrowing included), persistLocale stores the choice and syncs <html lang>, and setLocale lazy-loads that locale’s chunk on demand:

src/main.ts
import { resolveLocale, persistLocale } from 'verbaly';
import { setLocale, getLocale } from 'virtual:verbaly';

await setLocale(resolveLocale({ supported: ['en', 'es', 'pt'] }));
persistLocale(getLocale());

Migrate incrementally

No big-bang rewrite needed: the Verbaly runtime is ~3 KB, so both libraries can coexist while you move one page or namespace at a time. A pragmatic order:

  1. npx verbaly init + the build plugin, so the toolchain runs alongside i18next from day one.
  2. Port the catalogs: flatten keys, merge plural suffixes, drop the interpolation braces from four to two.
  3. Swap hooks page by page (useTranslationuseT) and delete each i18next namespace as it empties.
  4. Turn on the gate: npx verbaly check in CI, npx verbaly pseudo to catch hardcoded strings the migration missed.
Copied to clipboard