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:
| i18next | Verbaly |
|---|---|
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-languagedetector | resolveLocale() + persistLocale() |
i18next-http-backend (lazy load) | per-locale code-splitting, built into the plugin |
useTranslation() (react-i18next) | useT() (@verbaly/react) |
| Trans component | Trans component: values / components map over |
i18next.changeLanguage('es') | await setLocale('es') |
saveMissing, runtime fallbacks | npx 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:
{
"inbox": { "title": "Inbox" },
"messages_one": "one message",
"messages_other": "{{count}} messages"
}{
"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:
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
t('inbox.title');
t('messages', { count });import { useT } from '@verbaly/react';
const t = useT();
t('inbox.title');
t('messages', { count }); // params now typedNew 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.
t`Hola ${name}`; // key + types generated
t.id('inbox.title')`Inbox`; // readable id, i18next-styleLocale 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:
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:
npx verbaly init+ the build plugin, so the toolchain runs alongside i18next from day one.- Port the catalogs: flatten keys, merge plural suffixes, drop the interpolation braces from four to two.
- Swap hooks page by page (
useTranslation→useT) and delete each i18next namespace as it empties. - Turn on the gate:
npx verbaly checkin CI,npx verbaly pseudoto catch hardcoded strings the migration missed.