Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
How to pick the right Vue i18n library
"Vue i18n" is both a generic term and the name of the library nearly everyone installs. That is convenient and misleading at the same time: vue-i18n is a fine default, but it is not the only option, and the questions that should drive the choice (SSR or not, how many pages, who writes the translations) are rarely asked before npm install.
This guide asks them first, then maps the answers to the libraries that fit, for plain Vite + Vue and for Nuxt.

Table of Contents
Six questions to answer before comparing libraries
- Vite SPA or Nuxt? In an SPA the catalog cost is a JS bundle problem. In Nuxt it is also an HTML payload problem, because the messages are serialized into the SSR state and hydrated. Most "vue-i18n is slow" reports come from Nuxt apps for this reason.
- Who writes the translations? Developers, a TMS, an agency delivering ICU strings, or an AI pipeline.
vue-i18nuses its own pipe-separated plural syntax, not ICU. That matters if strings come from outside. - How many locales and pages? Two locales and five pages can ship everything. Ten locales and forty routes cannot, and the loading strategy becomes the main cost.
- Do you need types on keys?
t("cart.totl")compiles invue-i18nunless you pass a message schema generic, and that schema fights with lazily loaded catalogs. - What does the content contain? UI labels only, or markdown, links inside sentences, and per-locale blocks. Rich content is where
t()returning a string gets awkward. - Is CSP a constraint? The default
vue-i18nbuild compiles messages in the browser withnew Function. Runtime-only builds need@intlify/unplugin-vue-i18nto precompile at build time.
Write the answers down. Everything below refers back to them.
The landscape in one picture
The Vue ecosystem has fewer i18n libraries than React, and they come from different architectural waves.

vue-i18n appeared in 2015 and has been the default ever since. @nuxt/i18n wraps it with locale routing, SEO tags and lazy loading per locale. Messages are compiled to render functions, at build time if you add the unplugin, in the browser otherwise.
Mozilla Fluent .ftl files brought a friendlier message syntax with grammar-aware variants. No key types, and the Vite plugin loads every locale into every page.
Paraglide generates one function per message and lets the bundler tree-shake the rest. Intlayer declares content per component in .content.ts files, generates types, and ships only what a route renders.
The history of JavaScript i18n covers each wave in detail.
The decision that matters most: where content lives and when it loads
Two structural choices explain most of the bundle difference between setups:
- Centralized or scoped content. One
locales/en.jsonfor the app, or one declaration per component. - Static or dynamic import. Everything at startup, or the active locale (and ideally the active route) fetched on demand.
The graph estimates the payload for a theoretical app of 1 to 10 pages, translated into 1 to 10 locales, with about 30 KB of text per page.

vue-i18n supports the dynamic axis: setLocaleMessage after an import() means you stop shipping nine locales nobody reads. What it does not give you is the page axis. A locale catalog is one object, and loading it loads every page's copy. In an SPA nobody notices. In Nuxt, with @nuxtjs/i18n and more than ten pages, every route carries the strings of every other route, twice: in the JS chunk and in the SSR payload.
The Vue benchmark measures this as "leakage from other routes" and "leakage from other locales". If your answer to question 3 was "many pages", this section outweighs any API preference. The per-component vs centralized i18n post covers the maintenance side of the same trade-off.
The candidates
Library sizes are from the Vue benchmark: plugin plus composable in an empty component, after bundling, tree-shaking and minification, on a 10-page, 10-locale app. Content is measured separately.
Open the table in a modal to view all data content clearly
| Library | Content model | Types on keys | Message format | Per-route splitting | Library size |
|---|---|---|---|---|---|
vue-i18n | Central catalogs per locale, optional SFC <i18n> blocks | Opt-in via a schema generic | Own (pipe plurals) | No | ~24.3 kB |
@nuxtjs/i18n | Same as vue-i18n, plus routing and SEO tags | Same | Same | No, per locale only | On top |
fluent-vue | .ftl files (Mozilla Fluent) | None | Fluent | No | ~29.7 kB |
| Paraglide | inlang project, generated functions | Generated | Own | Via tree-shaking | Near zero |
| Intlayer | One .content.ts per component | Generated, on by default | Helpers (plural) | Yes, per component | Baseline |
Numbers are a snapshot at the benchmark's versions. Run it on your own app before deciding on size alone.
Paraglide's near-zero library size is by construction: the runtime is generated into your repository, which means a regeneration step before every push and merge conflicts on generated files. Intlayer needs vite-intlayer (or the Nuxt module), so it cannot run without a build step.
Match your answers to a library
vue-i18n in Composition mode (legacy: false), with @intlify/unplugin-vue-i18n so you ship the runtime-only build. Lazy-load locales with import(). That covers most small apps and the community answers are everywhere. SFC <i18n> blocks colocate messages with the component, which helps, but the extraction and TMS tooling around them is thinner than around JSON catalogs, so decide early which one the team uses.
@nuxtjs/i18n gives you the routing strategy, the hreflang tags and the locale detection with no code, and that alone justifies it for content sites with a handful of pages. Its limit is the per-locale catalog: past ten or so pages the SSR payload carries every route's copy. If that is your case, either hand-wire vue-i18n with per-route messages, or move to scoped content. The Nuxt i18n post walks through the routing strategy choice first.
vue-i18n's plural syntax ("no item | one item | {count} items") is not ICU and is not portable. Translators need to be told about it, and a TMS export will not produce it. Either agree on the format before the first catalog exists, or pick a library whose format matches your vendor. Intlayer's ICU support is partial, so if you receive ICU strings today, treat that as a blocker too.
Prefer scoped content compiled at build time. Paraglide gets there through tree-shaking, which works as advertised on Vite. Intlayer gets there through per-component declarations and ships only what the route renders. With vue-i18n, you can split messages by route by hand, but nothing enforces it and a shared component importing a global namespace quietly undoes it.
vue-i18n can be typed by passing a schema generic to createI18n. It works, and it breaks the moment catalogs are lazily loaded, because the schema describes messages that may not be there yet. If you do not want to maintain that, pick a library whose types are generated from the content: Paraglide or Intlayer. The detecting missing translations post compares what each catches at build time.
Markdown pages, sentences with a <RouterLink> in the middle, per-locale components. vue-i18n has <i18n-t> for component interpolation, which works and is verbose. Intlayer's content nodes accept markdown, HTML and nested objects directly, which fits better when the app is content-heavy.
Then the centralized JSON has no consumer left to justify it. Colocated content plus a CLI that fills the missing locales is the shorter path. Intlayer's fill command runs against your own API key (OpenAI, Anthropic, Mistral, Gemini) and only re-translates what changed.
Where each library falls short
vue-i18n: heaviest of the set, own plural format, types are opt-in and fragile with lazy loading, no per-route scoping, dead keys accumulate silently. Leavinglegacy: truein a Vue 3 app keeps the Vue 2 compatibility layer and losesuseI18n()typing.@nuxtjs/i18n: inherits everything above, and the SSR payload carries every page's strings once past a dozen routes.fluent-vue: nice message syntax, no key types, and the Vite plugin loads all content in all languages into every page. Heaviest in the benchmark.- Paraglide: generated files committed to the repo, regeneration before every push, and the locale is read from cookie or storage on each message call rather than from a reactive store, which costs work on locale change.
- Intlayer: mandatory build plugin, smaller ecosystem, partial ICU support, and content spread across the codebase by design, so exporting one JSON for a translator needs tooling.
What each option looks like in code
The same component, a cart summary with a title and a plural, written with each candidate. The interesting part is not the template, it is where the content lives and what vue-tsc knows about it.
Copy the code to the clipboard
Copy the code to the clipboard
Pipe-separated plurals are vue-i18n's own format, not ICU. t accepts any string unless you pass a message schema generic to createI18n.
Copy the code to the clipboard
Copy the code to the clipboard
Fluent's syntax handles plurals and grammatical variants well. Message ids are untyped strings, and the Vite plugin bundles every locale into every page.
Copy the code to the clipboard
Copy the code to the clipboard
Every message is a generated, typed function, so a missing key is an import error. The paraglide/ folder is generated into your repo and regenerated on every change.
Copy the code to the clipboard
Copy the code to the clipboard
All locales in one file beside the component. Types are generated at build, so title autocompletes and a typo fails vue-tsc. <title /> renders a node the visual editor can target; {{ items(props.count) }} gives the plain string.
Already on vue-i18n? The @intlayer/vue-i18n compat adapter aliases the package at the bundler level, so useI18n(), $t, pipe plurals and v-t keep working while Intlayer serves the content. The migration guide covers moving off the adapter afterwards, and there is a Nuxt-specific one.
Before you commit
A feature table tells you what a library does today. These points tell you what living with it will be like.
Check repository activity.
Commits, issue response time, and whether the last minor release was this year. A sound design with no maintainer is a migration in waiting.
Do not pick by npm downloads.
The most installed library is the one that shipped first, not the one that fits a 2026 Vue codebase. Downloads measure history, not fit.

Ask who pays the maintainer, and what they sell.
vue-i18n is backed by Crowdin, like next-intl and svelte-i18n. i18next is backed by Locize. Tolgee, Paraglide (inlang) and Intlayer each run their own platform. A vendor whose revenue is hosted translation has little reason to make translation free inside your toolchain. Intlayer is the only one of the set that ships AI translation through the CLI with your own API key, and a CMS you can self-host.
Is it AI-agent ready?
Agents still struggle with i18n: they forget locales, invent keys, and mix message syntaxes. Does the library ship Agent Skills or an MCP server so the agent can list, fill and test content? And is content loading optimized by default, or does someone have to review namespaces and lazy imports every quarter?
Type safety out of the box.
Not "can be typed with extra wiring" but "a wrong key fails tsc on a fresh install". Check what happens with a key that does not exist, and with a locale that is missing one translation.
Detection of unused content.
Catalogs only grow. Intlayer's build purges unused fields and logs them (build.purge). Paraglide gets there by architecture, since an uncalled message function is tree-shaken. Everything else leaves the sweep to you.
Developer experience.
Setup time to first translated string, an LSP or VS Code extension that shows the translation on hover and jumps to the declaration, a CLI for fill, test and push, and a way for non-developers to edit content (visual editor or CMS) without a pull request.
Frequently Asked Questions
For most Vue apps, yes. The ecosystem is the largest, the documentation is thorough, and the costs are predictable: a heavy runtime, a custom plural format, and per-route scoping that you have to build and defend yourself.
Use the module unless your routing is unusual or your app has few pages. Hand-wiring means rebuilding locale routes, middleware, hreflang and the sitemap yourself, and those are fiddlier than they look.
Only if bundle size, SSR payload, generated types or build-time missing-key checks are actual requirements. The compiler vs declarative i18n post explains what compilers give you and where they can get it wrong.
Indirectly. Crawlers care about routing, hreflang, <html lang> and whether text is in the server-rendered HTML. See the hreflang guide.
Going further
- Vue i18n benchmark: bundle size, leakage and locale-switch timings
- Vue i18n: how vue-i18n works and where it hurts and the Nuxt i18n post
- vue-i18n vs Intlayer, feature by feature and the vue-i18n vs Intlayer benchmark
- Is vue-i18n outdated?
- The history of JavaScript i18n
- Compiler vs declarative i18n
- Per-component vs centralized i18n
- Set up i18n in a Vite + Vue app and in a Nuxt app
- Same guide for React, Svelte and Solid
Comments
No comments yet. Be the first to share your thoughts.
