i18next-vue and vue-i18n are both internationalization (i18n) libraries designed to help Vue applications support multiple languages. vue-i18n is a Vue-specific solution built and maintained by the Vue core team, offering deep integration with Vue's reactivity system and developer experience. i18next-vue is a Vue adapter for the general-purpose i18next library, which is framework-agnostic and widely used across JavaScript ecosystems. While both enable translation, interpolation, and locale switching, they differ significantly in architecture, feature depth, and long-term maintainability considerations.
Both i18next-vue and vue-i18n bring internationalization (i18n) to Vue applications, but they do so with different philosophies, integration styles, and ecosystem assumptions. Let’s break down how they handle real-world scenarios developers face when building multilingual apps.
vue-i18n is built specifically for Vue and tightly integrated with its reactivity system. It provides a global $t method and a <i18n> component that work out of the box with Vue’s composition and options APIs.
// vue-i18n: Basic setup in main.js
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'en',
messages: {
en: { hello: 'Hello!' },
es: { hello: '¡Hola!' }
}
});
const app = createApp(App);
app.use(i18n);
app.mount('#app');
In components:
<!-- vue-i18n: Using $t in template -->
<template>
<p>{{ $t('hello') }}</p>
</template>
i18next-vue is a thin bridge between the general-purpose i18next library and Vue. It relies on i18next’s core functionality and adds Vue-specific helpers like the $t property and a useTranslation composable.
// i18next-vue: Setup
import { createApp } from 'vue';
import i18next from 'i18next';
import I18NextVue from 'i18next-vue';
i18next.init({
lng: 'en',
resources: {
en: { translation: { hello: 'Hello!' } },
es: { translation: { hello: '¡Hola!' } }
}
});
const app = createApp(App);
app.use(I18NextVue, { i18n: i18next });
app.mount('#app');
In components:
<!-- i18next-vue: Using $t in template -->
<template>
<p>{{ $t('hello') }}</p>
</template>
💡 Note: Both expose
$t, but under the hood,vue-i18nuses its own message resolver, whilei18next-vuedelegates toi18next.
vue-i18n automatically updates all translated content when you change i18n.global.locale. This works because it ties into Vue’s reactivity system.
// vue-i18n: Change language
const { t, locale } = useI18n();
locale.value = 'es'; // All $t() calls update instantly
i18next-vue also supports reactive updates, but only if you use the provided composables or directives. Changing i18next.language directly won’t trigger Vue updates unless you wrap it properly.
// i18next-vue: Reactive language switch
import { useTranslation } from 'i18next-vue';
const { t, i18n } = useTranslation();
i18n.changeLanguage('es'); // Triggers reactivity via internal watcher
If you bypass the composable and call i18next.changeLanguage('es') directly, your UI won’t update unless you manually force a refresh — a subtle but important gotcha.
vue-i18n uses a flat or nested object structure where keys map directly to messages. Interpolation and pluralization follow ICU-like syntax.
// vue-i18n: Pluralization
messages: {
en: {
item: 'You have {count} item | You have {count} items'
}
}
// Usage
$t('item', { count: 5 }) // → "You have 5 items"
i18next-vue inherits i18next’s powerful interpolation and pluralization rules, which are more flexible but require understanding i18next’s conventions.
// i18next: Pluralization (note the `_plural` suffix)
resources: {
en: {
translation: {
item: 'You have {{count}} item',
item_plural: 'You have {{count}} items'
}
}
}
// Usage
$t('item', { count: 5 }) // → "You have 5 items"
i18next also supports context, gender, and custom plural rules — useful for complex languages like Arabic or Russian.
vue-i18n supports fallback locales and missing key handling via the missing option:
createI18n({
missing: (locale, key) => {
console.warn(`Missing translation for ${key} in ${locale}`);
return key;
}
});
It also supports namespaced messages via message scope, but not true isolated namespaces like i18next.
i18next-vue leverages i18next’s full feature set:
common, auth, dashboard).// i18next: Namespace usage
$t('common:save') // Looks in the 'common' namespace
// With useTranslation
const { t } = useTranslation('auth');
t('login') // → auth:login
This makes i18next-vue better suited for large apps where translation files grow unwieldy.
vue-i18n offers first-class Composition API support with useI18n():
import { useI18n } from 'vue-i18n';
export default {
setup() {
const { t, locale } = useI18n();
return { t, switchLang: () => (locale.value = 'es') };
}
};
It also provides excellent TypeScript support with type-safe message keys when using @intlify/unplugin-vue-i18n.
i18next-vue provides useTranslation():
import { useTranslation } from 'i18next-vue';
export default {
setup() {
const { t, i18n } = useTranslation();
return { t, switchLang: () => i18n.changeLanguage('es') };
}
};
However, TypeScript inference for message keys isn’t built in — you’d need to augment types manually or use external tooling.
vue-i18n includes a dedicated devtools plugin and debug mode:
createI18n({
silentTranslationWarn: false,
warnHtmlInMessage: 'error'
});
i18next-vue benefits from i18next’s mature debugging tools:
i18next.init({
debug: true,
saveMissing: true,
missingKeyHandler: (lng, ns, key) => {
// Send missing keys to your translation management system
}
});
The saveMissing feature is especially valuable in production apps that collect untranslated phrases for later localization.
Choosing vue-i18n means you’re committing to a Vue-specific solution. If you ever move to React or Svelte, you’ll need to rewrite your i18n layer.
Choosing i18next-vue gives you a path to reuse your translation logic across frameworks, since i18next has official bindings for React (react-i18next), Angular, Svelte, and more.
Despite their differences, both libraries share essential capabilities:
Both provide $t in templates and composables for programmatic access.
<!-- Works in both -->
<template>
<h1>{{ $t('title') }}</h1>
</template>
Both allow runtime language changes with reactive updates (when used correctly).
Both support variable substitution:
// vue-i18n
$t('greeting', { name: 'Alice' }) // → "Hello, Alice!"
// i18next-vue
$t('greeting', { name: 'Alice' }) // → "Hello, Alice!"
Both offer directives for declarative translation:
<!-- vue-i18n -->
<p v-t="'message'" />
<!-- i18next-vue -->
<p v-t="'message'" />
| Feature | vue-i18n | i18next-vue |
|---|---|---|
| Core Engine | Built for Vue | Wrapper around i18next |
| Reactivity | Fully reactive by default | Reactive only via composables |
| Namespaces | Limited (scoped messages) | Full namespace support |
| Pluralization | Simple (ICU-like) | Advanced (per-language rules) |
| Dynamic Loading | Manual implementation needed | Built-in via backends |
| Cross-Framework | ❌ Vue-only | ✅ Reusable with other frameworks |
| TypeScript DX | Excellent (with tooling) | Basic (requires manual setup) |
| Missing Key Handling | Via missing callback | Via saveMissing + backend plugins |
Choose vue-i18n if you’re building a Vue-only app, want minimal setup, and value tight integration with Vue’s reactivity and tooling. It’s simpler for small to medium projects with straightforward i18n needs.
Choose i18next-vue if you need advanced features like namespaces, dynamic loading, or plan to share translation logic across multiple frameworks. It’s better for enterprise apps with complex localization workflows.
Both libraries are actively maintained and production-ready. The decision boils down to project scope and long-term strategy. For most Vue teams starting fresh, vue-i18n offers the smoothest ride. But if you’re already using i18next elsewhere or anticipate scaling your i18n infrastructure, i18next-vue pays off over time.
Choose i18next-vue if you need advanced i18n features like true namespaces, dynamic translation loading via backends, or cross-framework consistency (e.g., sharing logic with a React admin panel). It’s ideal for large-scale applications with complex localization workflows, teams already invested in the i18next ecosystem, or projects requiring fine-grained control over missing key handling and fallback strategies.
Choose vue-i18n if you’re building a Vue-only application and prioritize simplicity, tight integration with Vue’s reactivity system, and excellent TypeScript support out of the box. It’s well-suited for small to medium projects with straightforward translation needs, teams that value convention over configuration, and developers who want minimal setup with maximum Vue synergy.
This library is a simple wrapper for i18next, simplifying its use in Vue 3.
There is also a Vue 2 version of this package.
In the documentation, you can find information on how to upgrade from @panter/vue-i18next, from i18next-vue 3.x or earlier versions.
npm install i18next-vue
import Vue from "vue";
import i18next from "i18next";
import I18NextVue from "i18next-vue";
import App from "./App.vue";
/*const i18nInitialized = */i18next.init({ ... });
createApp(App).use(I18NextVue, { i18next }).mount('#app');
// to wait for loading the translations first, do this instead:
// i18nInitialized.then(() => createApp(App).use(I18NextVue, { i18next }).mount('#app'));
Use the $t translation function, which works like (and uses) the versatile t function from i18next.
There is a full tutorial for setting up i18next-vue. You can check out the live demo version version of it, too.
To learn about more options, check out the full documentation.
Given the i18next translations
i18next.init({
// ...
resources: {
en: {
// language
translation: {
// the default namespace
insurance: "Insurance",
},
},
de: {
// language
translation: {
// the default namespace
insurance: "Versicherung",
},
},
},
});
You can use
<template>
<h1>A test in {{ $i18next.language }}</h1>
<p>{{ $t("insurance") }}</p>
</template>
$t() works both in Options API and Composition API components.
Using the useTranslation() composition function you can access the i18next instance and t() in the setup part, and e.g. get a t() functions for a specific namespace.
<script setup>
import { computed } from "vue";
import { useTranslation } from "i18next-vue";
const { i18next, t } = useTranslation();
const term = computed(() => t("insurance"));
</script>
<template>
<h1>A test in {{ i18next.language }}</h1>
<p>inline: {{ t("insurance") }}</p>
<p>inline with $t: {{ $t("insurance") }}</p>
<p>computed: {{ term }}</p>
</template>
i18next-vue provides a <i18next> translation component, so you can use markup (including Vue components) in translations.
In this example {faq-link} will be replaced by the faq-link slot, i.e. by the router link. You can move {faq-link} around in the translation, so it makes sense for the target language.
i18next.init({
// ...
resources: {
en: {
translation: {
"message": "Open the {faq-link} page."
"faq": "Frequently Asked Questions"
}
},
fr: {
// ...
}
}
})
<template>
<i18next :translation="$t('message')">
<template #faq-link>
<router-link :to="FAQ_ROUTE">
{{ $t("faq") }}
</router-link>
</template>
</i18next>
</template>
Custom slot values may be useful when the default braces ({ and }) are wrongly treated by the
Locize service or don't satisfy other needs.
Use custom values for recognizing start and end of the insertion points of the <i18next>/TranslationComponent
inside the localization term:
// main.js
i18next.init({
// ...
resources: {
en: {
translation: {
"message": "Open the <slot>faq-link</slot> page."
"faq": "FAQ"
}
},
de: {
// ...
}
}
})
app.use(I18NextVue, {
i18next,
slotStart: '<slot>',
slotEnd: '</slot>',
});
<!-- Component.vue -->
<template>
<i18next :translation="$t('message')">
<template #faq-link>
<router-link :to="FAQ_ROUTE">
{{ $t("faq") }}
</router-link>
</template>
</i18next>
</template>