Author:
    Creation:2026-09-13Last update:2026-09-13

    vue-i18n VS Intlayer | Vue Internationalisation (i18n) Benchmark

    vue-i18n is the reference i18n library for Vue. Intlayer is a compiler-based, component-scoped alternative with a Vue integration (vue-intlayer). We already compared their features and developer experience. This article looks at what each one costs once the app is built.

    The data comes from Benchmark Bloom, an open-source suite that builds the same application with each library and records what the browser actually downloads and executes.

    tl;dr: On the same Vite + Vue 3 app, vue-i18n ships 134.9 KB of gzipped JavaScript per page against 41.3 KB for the app without i18n. Intlayer ships 57.1 KB. The vue-i18n runtime alone weighs 24.3 KB gzip (6x Intlayer's 3.9 KB), every page carries 90% of foreign-page strings, and a component compiled in isolation drags in 196 KB because it is bound to the global message tree. The @intlayer/vue-i18n adapter keeps the vue-i18n API and measured 47.0 KB per page.

    In short

    • vue-i18n - The de-facto i18n library for Vue 2 / Vue 3 and the core of @nuxtjs/i18n. ICU-style messages, SFC <i18n> blocks, v-t directive, d() / n() formatters, large ecosystem. Messages are registered on a global instance at createI18n(); lazy loading per locale is a manual setLocaleMessage() pattern, and per-route splitting is yours to build.
    • Intlayer - Component-centric content model. .content.ts dictionaries sit next to the component they serve, a build-time compiler (vite-intlayer) tree-shakes and lazy-loads them per component and per locale, strict TypeScript types are generated from your content, and missing translations fail at build time. Ships router / SEO helpers, a Visual Editor / CMS and AI-assisted translation.
    LibraryGitHub StarsTotal CommitsLast CommitFirst VersionNPM VersionNPM Downloads
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitApril 2024npmnpm downloads
    intlify/vue-i18nGitHub Repo starsGitHub commit activityLast CommitDec 2016npmnpm downloads
    Badges update automatically. Snapshots will vary over time.

    Side-by-side feature comparison

    Featurevue-intlayer (Intlayer)vue-i18n
    Translations near components✅ Yes, .content.ts collocated with each component✅ Via SFC <i18n> blocks (optional); global catalogues are the common setup
    TypeScript integration✅ Strict types auto-generated from content✅ Good typings; strict key safety needs schema typing and discipline
    Missing translation detection✅ TypeScript error + build-time error/warning⚠️ Runtime fallback + console warning
    Rich content (components / Markdown)✅ Direct support⚠️ <i18n-t> component interpolation; Markdown via external plugins
    ICU support⚠️ WIP✅ Yes
    Formatting (dates, numbers, currencies)✅ Intl-based formattersd() / n() with datetimeFormats / numberFormats
    Localised routing✅ Helpers for Vue Router / Nuxt, getMultilingualUrls⚠️ Not core (@nuxtjs/i18n or custom router setup)
    SEO helpers (hreflang, sitemap, robots)✅ Built-in helpers❌ Not core
    Tree-shaking (ship only used content)✅ Per component, per locale, automated by the compiler⚠️ Manual: split catalogues, setLocaleMessage() per route
    Lazy loadingimportMode: 'dynamic' (one line of config)✅ Manual import() + setLocaleMessage()
    Purge unused content✅ Dead dictionaries are dropped at build time❌ Not built-in
    Testing missing translations (CLI / CI)npx intlayer content test⚠️ Third-party (vue-i18n-extract)
    AI-powered translation✅ Built-in, uses your own provider keys❌ No
    Visual Editor / CMS✅ Free Visual Editor + optional CMS❌ No (external localisation platforms)
    MCP server & Agent Skills✅ Yes❌ No
    Ecosystem / community⚠️ Smaller but growing fast✅ Large and mature in the Vue ecosystem

    The benchmark

    What was measured

    The Benchmark Bloom suite builds the same Vite + Vue 3 application with each library: 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 libraries were tested in the static configuration, the one most Vue projects ship: for vue-i18n, every locale's JSON imported and passed to createI18n({ messages }); for Intlayer, the default importMode: 'static'. In that mode Intlayer bundles every locale too, but the compiler still scopes content per component, so a page only carries the dictionaries of the components it renders.

    For each build, the suite records:

    • Lib size: gzip size of an empty component that only imports the i18n library. The fixed cost of the runtime.
    • Page JS: gzip JavaScript downloaded per page, averaged over all pages and locales.
    • Locale leak %: share of translated strings found in the downloaded JS that belong to a locale the user is not viewing (fingerprinted on en and fr, so 50% means "the other measured locale is fully present"; with 10 locales bundled, the real waste is higher).
    • Page leak %: share of translated strings found 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. Shows how much i18n runtime and catalogue a single component drags in.
    • 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 with vue-i18n 11.4.0 and intlayer 9.5.0 / 9.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 whilst the runtime cost stays fixed.

    Results on Vite + Vue 3

    LibraryStrategyLib size (gz)Lib size (min)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityPage load
    base (no i18n)-0.0 KB0.0 KB41.3 KB0.0%-1.1 KB1.8 ms10.8 ms
    vue-i18nstatic24.3 KB83.2 KB134.9 KB50.0%90.0%196.0 KB2.8 ms13.6 ms
    vue-intlayerstatic3.9 KB11.1 KB57.1 KB56.8%0.0%7.7 KB4.5 ms13.8 ms
    @intlayer/vue-i18n (compat)static7.9 KB23.2 KB47.0 KB15.0%0.0%8.4 KB1.5 ms9.3 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

    • Runtime cost. vue-i18n is one of the heaviest runtimes in the whole benchmark: 24.3 KB gzip / 83.2 KB minified for an empty component that only imports it. vue-intlayer costs 3.9 KB gzip. That gap is paid on every page regardless of how many strings you have.
    • Per-page JavaScript. The app without i18n weighs 41.3 KB. vue-i18n more than triples it to 134.9 KB; Intlayer lands at 57.1 KB, +15.8 KB, most of which is the ten bundled locales (see the next point).
    • Leakage. With createI18n({ messages: { en, fr, ... } }), every page ships every locale and every page's strings: 50% locale leakage (on the two fingerprinted locales) and 90% page leakage. Intlayer's static mode also bundles every locale (hence the comparable locale-leak figure) but has 0% page leakage: a page only pulls the dictionaries of the components it renders. Switching to importMode: 'dynamic' removes the locale leakage as well; that configuration was not part of this Vue run.
    • Component size is where the architecture shows. A component calling useI18n() compiles to 196 KB on average, because t() is bound to the global instance that holds every message of every locale. The same component with useIntlayer() compiles to 7.7 KB: it only reaches its own dictionary.
    • Reactivity is a non-issue for both (2-5 ms). Vue's reactivity system makes locale switching cheap once the messages are in memory.
    • @intlayer/vue-i18n, the drop-in adapter, keeps the vue-i18n API and measured 47.0 KB per page and 8.4 KB per component, with the application code untouched.
    For reference, the same run measured fluent-vue at 171.8 KB per page, 29.7 KB of runtime and 217 KB per component.

    Why the gap? Global instance vs. compiled dictionaries

    vue-i18n is a runtime. createI18n() builds a global instance holding a message tree per locale; useI18n() binds each component to it; t("footer.github") looks the key up at render time. This is what makes SFC <i18n> blocks, v-t, and runtime message loading possible, and it is also why every component's dependency graph includes the whole tree:

    bash
    .
    ├── locales
       ├── en.json
       ├── fr.json
       └── ...                        # one file per locale, all pages inside
    └── src
        ├── i18n.ts                    # createI18n({ messages: { en, fr, ... } })
        ├── main.ts
        └── components
            └── Footer.vue             # const { t } = useI18n(); t("footer.github")
    

    Optimising means you split en.json into per-route files, you call setLocaleMessage() in a router guard, and you keep the route-to-file map correct as components move. The runtime can't do it for you because it has no idea which keys a component will ask for.

    Intlayer moves that knowledge to the build. Content is declared next to the component, and vite-intlayer resolves which component imports which dictionary:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── main.ts                    # createApp(App).use(intlayer)
        └── components
            └── Footer
                ├── Footer.vue         # useIntlayer("footer")
                └── Footer.content.ts
    

    The compiler emits, per dictionary and per locale, exactly the JSON that component needs, and drops dictionaries nothing imports. Per-route scoping is a consequence of per-component scoping, not a task.

    To also drop the unused locales, set dictionary.importMode: 'dynamic' in intlayer.config.ts. See the bundle optimisation doc.

    Developer experience

    Setup

    vue-i18n

    src/i18n.ts
    import { createI18n } from "vue-i18n";
    import en from "../locales/en.json";
    import fr from "../locales/fr.json";
    
    export const i18n = createI18n({
      legacy: false,
      locale: "en",
      fallbackLocale: "en",
      messages: { en, fr },
    });
    
    src/main.ts
    import { createApp } from "vue";
    import App from "./App.vue";
    import router from "./router";
    import { i18n } from "./i18n";
    
    createApp(App).use(router).use(i18n).mount("#app");
    

    Intlayer

    intlayer.config.ts
    import { type IntlayerConfig, Locales } from "intlayer";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [Locales.ENGLISH, Locales.FRENCH],
        defaultLocale: Locales.ENGLISH,
      },
    };
    
    export default config;
    
    vite.config.ts
    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    import { intlayer } from "vite-intlayer";
    
    export default defineConfig({
      plugins: [intlayer(), vue()],
    });
    
    src/main.ts
    import { createApp } from "vue";
    import { intlayer } from "vue-intlayer";
    import App from "./App.vue";
    import router from "./router";
    
    createApp(App).use(intlayer).use(router).mount("#app");
    

    Component

    vue-i18n

    locales/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.vue
    <script setup lang="ts">
    import { ref } from "vue";
    import { useI18n } from "vue-i18n";
    
    const { t, n } = useI18n();
    const count = ref(0);
    </script>
    
    <template>
      <div>
        <p>{{ n(count) }}</p>
        <button :aria-label="t('counter.label')" @click="count++">
          {{ t("counter.increment") }}
        </button>
      </div>
    </template>
    

    t('counter.label') is a string until you type the message schema yourself; a typo renders the key.

    Intlayer

    src/components/Counter/Counter.content.ts
    
    

    Comments

    No comments yet. Be the first to share your thoughts.

    Related Posts

    Last Posts