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
vue-i18n VS @intlayer/vue-i18n | Same API, Different Bundle
@intlayer/vue-i18n is a compat adapter: it exposes the vue-i18n API (createI18n, useI18n, t(), d(), n(), $t, v-t, i18n.global.locale...) and serves it from dictionaries compiled by Intlayer. Your .vue files do not change. What t("footer.github") is bound to does.
This article measures that swap on the same Vite + Vue 3 application, built once with vue-i18n and once with the adapter. The numbers come from Benchmark Bloom. For vue-i18n and Intlayer compared as libraries, read vue-i18n vs Intlayer and the vue-i18n vs Intlayer benchmark. This one is about what the adapter changes when you keep your components as they are.
tl;dr: On the same Vite + Vue 3 app, replacingvue-i18nwith@intlayer/vue-i18ntook the per-page JavaScript from 134.9 KB to 47.0 KB gzip (the app without i18n weighs 41.3 KB), the runtime from 24.3 KB to 7.9 KB, the average component from 196 KB to 8.4 KB, and foreign-page string leakage from 90% to 0%, with no.vuefile edited.createI18n({ messages })keeps working as a fallback; remove the JSON imports to get the numbers above. SFC<i18n>blocks and runtimesetLocaleMessage()are the two features that do not carry over.
What @intlayer/vue-i18n is
vue-i18n is a runtime. createI18n({ messages: { en, fr, ... } }) builds a global instance holding every message of every locale; useI18n() binds each component to it; t("footer.github") walks the tree at render time. That design is what makes SFC <i18n> blocks and setLocaleMessage() possible, and it is also why every component's dependency graph includes the whole tree.
@intlayer/vue-i18n keeps the API and replaces the tree:
- Import aliasing.
vueI18nVitePlugin()from@intlayer/vue-i18n/pluginwrapsvite-intlayerand adds aresolve.aliasso thatvue-i18nresolves to@intlayer/vue-i18n. No import is renamed. - JSON as source of truth. The
syncJSONplugin reads your existinglocales/{locale}.jsonwithformat: "vue-i18n"(so{name},{0}list interpolation and"car | cars"pipe plurals are parsed correctly) and writes translations back when the CLI or the CMS updates them. - Call-site binding. The Intlayer optimize pass rewrites
useI18n()call sites so the component receives the dictionaries its keys name, in the active locale, as imports the bundler can trace and split.
Copy the code to the clipboard
Copy the code to the clipboard
The component no longer reaches the global message tree. It reaches footer. That is why the component-size column below drops from 196 KB to 8 KB.
What the adapter keeps, ignores, and does not replace
Open the table in a modal to view all data content clearly
vue-i18n API | With @intlayer/vue-i18n |
|---|---|
useI18n() → { t, d, n, te, tm, rt, locale, availableLocales } | ✅ Kept. t keys are typed against your dictionaries |
t("key", { name }), t("key", [a, b]), t("key", count) | ✅ Kept. {name}, {0} and pipe-separated plurals resolve as before |
d(date, "long"), n(value, "currency") | ✅ Kept. datetimeFormats / numberFormats from createI18n() are honored, backed by native Intl |
i18n.global.locale.value = "fr" | ✅ Kept. A WritableComputedRef backed by Intlayer's client; reactivity behaves as before |
$t, $tc, $te, $tm, $rt, $d, $n, $i18n (Options API) | ✅ Kept. Registered on app.config.globalProperties by app.use(i18n) |
v-t directive | ✅ Kept |
legacy: true | ✅ Accepted |
createI18n({ messages }) | ⚠️ messages are used as a runtime fallback with a dev warning. Remove the JSON imports for the bundle gains |
setLocaleMessage(), mergeLocaleMessage() | ❌ Warn and do nothing. Runtime message loading is replaced by build-time dictionaries |
SFC <i18n> custom blocks | ❌ Not read. Move those messages into the locale JSON (or a .content.ts next to the component) |
@nuxtjs/i18n | ⚠️ Separate adapter, see the Nuxt compat doc |
The benchmark
What was measured
The Benchmark Bloom suite builds the same Vite + Vue 3 application with each setup: 10 pages (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 locales (en, fr, es, de, it, pt, zh, ja, ko, ru), identical components and identical content. Pages are measured in en and fr.
Both were built in the static configuration, the one most Vue projects ship: for vue-i18n, every locale's JSON imported and passed to createI18n({ messages }); for the adapter, the same components with vite.config.ts and intlayer.config.ts changed and the messages import removed. Native vue-intlayer is included for reference.
For each build, the suite records:
- Lib size: gzip (and minified) size of an empty component that only imports the i18n library.
- Page JS: gzip JavaScript downloaded per page, averaged over all pages and locales.
- Locale leak %: share of translated strings in the downloaded JS that belong to a locale the user is not viewing.
- Page leak %: share of translated strings in the downloaded JS that belong to a page the user is not on.
- Component avg: average gzip size of each component compiled in isolation.
- E2E reactivity: wall-clock time between selecting a new locale and
html[lang]updating in the DOM (Playwright, 5 iterations). - Page load:
PerformanceNavigationTiming.duration.
Numbers below come from the run dated 2026-09-12 withvue-i18n11.4.0 and@intlayer/vue-i18n9.5.1. The test application is deliberately small (a few dozen strings per locale), so leakage percentages describe a pattern: they grow with your content while the runtime cost stays fixed.
Results on Vite + Vue 3
Open the table in a modal to view all data content clearly
| Setup | Strategy | Lib size (gz) | Lib size (min) | Page JS avg (gz) | Locale leak | Page leak | Component avg (gz) | E2E reactivity | Page load |
|---|---|---|---|---|---|---|---|---|---|
| base (no i18n) | - | 0.0 KB | 0.0 KB | 41.3 KB | 0.0% | - | 1.1 KB | 1.8 ms | 10.8 ms |
vue-i18n | static | 24.3 KB | 83.2 KB | 134.9 KB | 50.0% | 90.0% | 196.0 KB | 2.8 ms | 13.6 ms |
@intlayer/vue-i18n | static | 7.9 KB | 23.2 KB | 47.0 KB | 15.0% | 0.0% | 8.4 KB | 1.5 ms | 9.3 ms |
vue-intlayer (native) | static | 3.9 KB | 11.1 KB | 57.1 KB | 56.8% | 0.0% | 7.7 KB | 4.5 ms | 13.8 ms |
vue-intlayer (native) | dynamic | 3.9 KB | 11.1 KB | 59.8 KB | 50.0% | 0.0% | 6.5 KB | 4.0 ms | 15.8 ms |
The base app's page-leak column is left blank: with no i18n library, the fingerprinting picks up hard-coded strings in shared chunks and the number is not meaningful.
How to read it
- 88 KB less per page, same components.
vue-i18ntakes the 41.3 KB app to 134.9 KB. The adapter build of the same components lands at 47.0 KB, 5.7 KB over the base app. Most of the difference is the 74.9 KB ofsrc/localesthatcreateI18n({ messages })pulls into every page and the adapter never bundles as a block. - The runtime shrinks 3x. An empty component that only imports
vue-i18ncosts 24.3 KB gzip / 83.2 KB minified:@intlify/core-base, the message compiler and the runtime. The adapter costs 7.9 KB / 23.2 KB, most of it Intlayer's core plus thevue-i18nAPI surface. - Components: 23x smaller. A
useI18n()component compiled in isolation averages 196 KB, becausetis bound to the instance that holds every message of every locale. With the adapter, the same component averages 8.4 KB: it reaches its own dictionary. - Leakage.
vue-i18nships every locale and every page's strings on every page: 50% locale leakage (on the two fingerprinted locales; with ten locales bundled the real waste is higher), 90% page leakage. The adapter drops page leakage to 0% because each component only imports its dictionaries. Locale leakage sits at 15% in thisstaticrun;importMode: 'dynamic'is the setting that removes it, and that configuration was not part of this Vue run. - Reactivity and page load. Locale switching is cheap for both (1.5-2.8 ms); Vue's reactivity system makes it so once messages are in memory. Page load goes from 13.6 ms to 9.3 ms, in line with 88 KB less JavaScript to parse.
- About the native rows.
vue-intlayerin this run bundled every locale instaticmode and landed at 57.1 KB with a 3.9 KB runtime; the adapter's synced dictionaries carried fewer foreign-locale strings, hence the lower per-page figure. The native runtime remains the lightest of the three, and its.content.tsmodel is where SFC<i18n>blocks find their equivalent.
Why the numbers move
Nothing in src/components/ changed, so the gains come from what useI18n is bound to.
With vue-i18n, the binding is the global instance. createI18n({ messages: { en, fr, ... } }) is one import that holds everything; every component that calls useI18n() can reach all of it, so the bundler cannot split below the instance. Optimizing means you split en.json by route, call setLocaleMessage() in a router guard, and keep the route-to-file map correct as components move.
Copy the code to the clipboard
With @intlayer/vue-i18n, the binding is the dictionary. syncJSON turns each top-level key of en.json into a dictionary; the optimize pass hands the component the ones its keys name, as imports the bundler traces and splits per page.
Copy the code to the clipboard
The messages import in i18n.ts is the one line to delete. That is the 88 KB.
Migration in three steps
Install
bashCopy codeCopy the code to the clipboard
The command detects
vue-i18n, installsintlayer,vue-intlayer,@intlayer/vue-i18nand@intlayer/sync-json-plugin, and pre-fillsintlayer.config.ts. Keepvue-i18ninstalled: it is a peer dependency and provides the types.Point Intlayer at your locale files
intlayer.config.tsCopy codeCopy the code to the clipboard
locales/{locale}.jsonstays where it is. Each top-level key (footer,hero...) becomes a dictionary.Add the plugin and drop the messages import
vite.config.tsCopy codeCopy the code to the clipboard
src/i18n.tsCopy codeCopy the code to the clipboard
vueI18nVitePlugin()wrapsvite-intlayer(content watching, dictionary compilation, the optimize pass) and aliasesvue-i18nto the adapter. Removing themessagesimport is what drops the 88 KB; leaving it in keeps the app working but ships both.
What you can delete afterwards
Open the table in a modal to view all data content clearly
| File / pattern | Why |
|---|---|
import en from "./locales/en.json" and friends | Used only as a fallback by the adapter. This is where the 88 KB was |
setLocaleMessage() in router guards | No-op. Per-route loading is the compiler's job now |
@intlify/unplugin-vue-i18n | Not needed: it precompiles messages and SFC blocks the adapter does not read |
SFC <i18n> blocks | Not read; move them to the locale JSON or to a .content.ts per component |
What you gain beyond bytes
- Typed keys.
t("footer.github")is typed against the compiledfooterdictionary; a wrong path is a TypeScript error instead of the key rendered as text. npx intlayer testfails CI on a missing key in any locale.npx intlayer filltranslates the missing ones with your own provider key (OpenAI, Anthropic, Mistral, Gemini...) and writes them back intolocales/{locale}.json.- Visual Editor and CMS operate on the same JSON, so non-developers edit through a UI and the files update.
- Incremental move to
.content.ts. Any component can switch fromuseI18n()touseIntlayer("footer")with a co-located content file. JSON and.content.tsdictionaries coexist and merge.
Limits to know before you start
- SFC
<i18n>blocks are not read. If your messages live inside components, they need to move to the locale files (or to.content.ts, which is the same idea with types). - Runtime message loading is gone.
setLocaleMessage()andmergeLocaleMessage()warn and return. Translations fetched from a CMS at runtime need Intlayer's CMS, or theintlayer pull/pushcommands. messagesis a fallback, not free. Keeping the JSON imports increateI18n()keeps the 75 KB in the bundle. Delete them onceintlayer testpasses.- The adapter is not the native runtime. 7.9 KB against 3.9 KB for
vue-intlayer. Once every component has moved touseIntlayer, drop it.
When to use which?
- Stay on
vue-i18nif your app depends on SFC<i18n>blocks, on runtimesetLocaleMessage()flows, or if 90 KB per page is not a concern for your audience. - Use
@intlayer/vue-i18nif you are onvue-i18nand want the 88 KB, the 23x smaller components, 0% page leakage, typed keys and CI checks without editing a.vuefile. This is the entry point for an existingvue-i18ncodebase. - Go native (
vue-intlayer) for new projects, or once the adapter has done its job. It has the lightest runtime (3.9 KB) and the per-component.content.tsmodel that replaces<i18n>blocks with typed content.
Related comparisons
- vue-i18n vs Intlayer (features and DX)
- vue-i18n vs Intlayer benchmark (the libraries, same benchmark)
- next-intl vs @intlayer/next-intl (same adapter series)
- i18next vs @intlayer/i18next (same adapter series)
- Lingui vs @intlayer/lingui (same adapter series)
- Migration guide: vue-i18n to Intlayer
- Compat adapter reference: vue-i18n, Nuxt i18n
Conclusion
@intlayer/vue-i18n changes what useI18n() is bound to: from a global instance holding every message of every locale to a dictionary compiled for that component. On the same Vite + Vue 3 app that is 88 KB less per page, a 3x smaller runtime, 23x smaller components and 0% page leakage, for a config file, a plugin line and one deleted import. SFC <i18n> blocks and runtime message loading are the two things it does not carry, and the native vue-intlayer runtime remains half its size.
All the raw data, the test apps and the scripts are in the Benchmark Bloom repository. Run it yourself.
Refer to the 'Why Intlayer?' doc for more details.
Comments
No comments yet. Be the first to share your thoughts.
