When building internationalized apps with Vue 3 or Nuxt, vue-i18n has long been the automatic default. Kazupon and the community built a mature, feature-rich ecosystem, and for years it was the go-to standard for localizing Vue applications.
To understand why vue-i18n behaves the way it does, look at when its architecture was designed: it was conceived for the classic Single Page Application (SPA) era, back before dynamic imports (import()) and route-level code splitting were widely supported. In that paradigm, loading a single, monolithic translation object into memory at startup made complete sense.
However, modern frontend development is centered around aggressive page-size optimization, whether you're leveraging Nuxt, static site generation (SSG), or fine-grained dynamic page loading. In this modern context, that centralized, global provider model has become a severe architectural bottleneck.
The library footprint itself is surprisingly heavy: an empty component that only imports vue-i18n costs 24.3 KB gzipped (83.2 KB minified) before rendering a single word. Why is the library so big? Because it ships an entire runtime engine to process message format logic on the fly, evaluating dynamic insertions like {{number}} or {name}, pluralization rules, and list formatting in the browser. On top of that, handling localized routing and URL management requires shipping extra client-side logic for cookie storage, locale detection, and URL prefixing.
And that's before accounting for the bigger architectural flaw: massive copy leakage across routes.
In typical Vue apps, createI18n({ messages }) instantiates a global message tree containing every translation key for every supported language. The moment a user visits a simple /contact page, the browser is forced to download strings for /dashboard, /pricing, /settings, and the rest of the application. In our benchmarks on a standard 10-route, 10-locale Vite + Vue 3 app, 90% of the translation payload downloaded on a given page belonged to completely unrelated routes. Because useI18n() binds components to this global instance, compiling a single component in isolation dragged in an average of 196 KB of JavaScript.
Furthermore, because t("key.path") relies on dynamic runtime string evaluation, bundlers like Vite and Rollup cannot statically trace which keys are actually used. Unused translations cannot be tree-shaken, and key typos silently fail at runtime instead of being caught by TypeScript at build time.
How does Intlayer fix this?
Intlayer fixes this by eliminating all this extra logic at build time and connecting content directly to its related components through static transformations rather than relying on bloated runtime parsers and providers.
1. Build-time pre-compilation (no runtime parsing overhead)
Instead of sending heavy parsers to the browser to evaluate message insertions like {{number}} or manage cookie storage and URL prefixing at runtime, Intlayer resolves and optimizes this logic during build time. The browser receives lean, pre-compiled code with all extra machinery stripped away, cutting the runtime footprint down to 3.9 KB (or 7.9 KB with the compat adapter).
2. Direct component-to-content binding
Instead of dumping all strings into monolithic locales/en.json files, Intlayer collocates dictionaries directly next to the components that consume them (e.g., Footer.content.ts next to Footer.vue). As a result, components that are not imported never load unused content. Dead code and unrendered components carry zero translation weight.
3. Zero-waterfall dynamic loading with Vite
Even more powerfully, Intlayer leverages Vite's advanced module transformations. Even when using dynamic page loading and code splitting, Intlayer attaches only the localized content consumed in that specific chunk directly into the bundle. There are no extra network requests or waterfalls awaiting remote JSON dictionaries before rendering. The component and its exact localized strings arrive together in a single chunk.
4. Strict compile-time safety
Intlayer generates strict TypeScript definitions directly from your content declarations. You get IDE autocompletion for keys and immediate build-time errors for missing translations or typos, eliminating runtime fallback surprises in production.
5. 0% leakage and 3x smaller bundles
At build time, the Intlayer compiler statically analyzes call sites and splits dictionaries so each route only downloads the strings it actually renders. In our benchmarks, page copy leakage dropped from 90% down to 0%, cutting per-page JavaScript from 134.9 KB to 47.0 KB gzipped (for reference, the baseline app without any i18n weighs 41.3 KB).
6. Drop-in migration via @intlayer/vue-i18n
If you have an existing Vue codebase, you don't need to rewrite your components. The @intlayer/vue-i18n compat adapter exposes the exact same vue-i18n API (useI18n, t(), d(), n(), $t, v-t). By adding the Vite plugin and dropping the monolithic messages import, your existing t("key") calls automatically bind to compiled, split dictionaries, shrinking your runtime 3x and your components 23x with zero .vue file edits.
If you are running a localized Vue or Nuxt app in production, check your browser's Network tab on any subpage: you will likely find that most of your downloaded JavaScript is translation copy users will never read.
Read the complete breakdown, benchmarks, and migration guides here:
https://intlayer.org/blog/vue-i18n-vs-intlayer
https://intlayer.org/blog/vue-i18n-vs-intlayer-vue-i18n


Top comments (0)