IntlPull
Tutorial
18 min read

Vue.js Localization: Complete Guide to Vue i18n (2026)

Master Vue.js localization with vue-i18n. Step-by-step tutorial covering setup, dynamic translations, pluralization, lazy loading, and production deployment.

IntlPull Team
IntlPull Team
03 Feb 2026, 11:44 AM [PST]
On this page
Summary

Master Vue.js localization with vue-i18n. Step-by-step tutorial covering setup, dynamic translations, pluralization, lazy loading, and production deployment.

Quick Answer

To localize a Vue app, use vue-i18n (v9). Install with npm install vue-i18n, create translation JSON files in a locales directory, configure the createI18n instance in your main.ts, and use the $t() helper in templates or useI18n() composable in scripts. For Nuxt 3, use the @nuxtjs/i18n module for built-in SSR, routing and SEO support.


The Problem

You've built a beautiful Vue app. Now your product team says "we need to support Spanish, French, and German."

Where do you even start? How do you:

  • Extract all those hardcoded strings?
  • Switch languages without page refreshes?
  • Handle plurals differently in each language?
  • Lazy load translations to keep your bundle small?

This guide walks through everything, using vue-i18n, the official Vue localization library.

What You'll Build

By the end of this tutorial, you'll have a Vue app that:

  • ✅ Switches languages instantly
  • ✅ Handles plurals correctly in 40+ languages
  • ✅ Formats dates, numbers, and currencies by locale
  • ✅ Lazy loads translations for performance
  • ✅ Works with Vue Router for SEO-friendly URLs
  • ✅ Syncs with a translation management system

Tech stack:

  • Vue 3 (Composition API)
  • vue-i18n v9
  • Vite
  • TypeScript (optional but recommended)

Step 1: Install vue-i18n

First, install the library:

Terminal
npm install vue-i18n@9

Why vue-i18n? It's the official i18n plugin for Vue, maintained by the Vue team. It has:

  • 5M+ weekly downloads
  • Built-in support for Vue 3's Composition API
  • SSR support for Nuxt
  • ICU message format (same as React/Angular)

Step 2: Create Translation Files

Create a locales folder with your translation files:

src/
├── locales/
│   ├── en.json
│   ├── es.json
│   └── fr.json
└── i18n.ts

en.json:

JSON
1{
2  "nav": {
3    "home": "Home",
4    "about": "About",
5    "pricing": "Pricing"
6  },
7  "hero": {
8    "title": "Build apps faster",
9    "subtitle": "Ship features your users love",
10    "cta": "Get started"
11  },
12  "product": {
13    "addToCart": "Add to cart",
14    "itemCount": "No items | {n} item | {n} items",
15    "price": "{amount} per month"
16  }
17}

es.json:

JSON
1{
2  "nav": {
3    "home": "Inicio",
4    "about": "Acerca de",
5    "pricing": "Precios"
6  },
7  "hero": {
8    "title": "Crea aplicaciones más rápido",
9    "subtitle": "Lanza funciones que tus usuarios aman",
10    "cta": "Empezar"
11  },
12  "product": {
13    "addToCart": "Añadir al carrito",
14    "itemCount": "Sin artículos | {n} artículo | {n} artículos",
15    "price": "{amount} por mes"
16  }
17}

Key structure tips:

  1. Nest by feature (nav, hero, product) not by page
  2. Use semantic keys (addToCart) not UI positions (button_1)
  3. Consistent formatting across all languages

Step 3: Configure vue-i18n

Create src/i18n.ts:

TypeScript
1import { createI18n } from 'vue-i18n';
2import en from './locales/en.json';
3import es from './locales/es.json';
4import fr from './locales/fr.json';
5
6export const i18n = createI18n({
7  legacy: false, // Use Composition API mode
8  locale: 'en', // Default language
9  fallbackLocale: 'en', // Fallback if translation missing
10  messages: {
11    en,
12    es,
13    fr,
14  },
15  // Enable warnings in dev
16  missingWarn: import.meta.env.DEV,
17  fallbackWarn: import.meta.env.DEV,
18});

Why legacy: false? This enables Composition API support. If you're using Vue 3, always set this.


Step 4: Add to Your Vue App

In main.ts:

TypeScript
1import { createApp } from 'vue';
2import App from './App.vue';
3import { i18n } from './i18n';
4
5const app = createApp(App);
6app.use(i18n);
7app.mount('#app');

That's it. Now every component has access to translations.


Step 5: Use Translations in Components

Basic Usage (Composition API)

VUE
1<script setup>
2import { useI18n } from 'vue-i18n';
3
4const { t } = useI18n();
5</script>
6
7<template>
8  <nav>
9    <a href="/">{{ $t('nav.home') }}</a>
10    <a href="/about">{{ $t('nav.about') }}</a>
11    <a href="/pricing">{{ $t('nav.pricing') }}</a>
12  </nav>
13
14  <section>
15    <h1>{{ $t('hero.title') }}</h1>
16    <p>{{ $t('hero.subtitle') }}</p>
17    <button>{{ $t('hero.cta') }}</button>
18  </section>
19</template>

With Interpolation

Pass variables using the second argument:

VUE
1<script setup>
2const { t } = useI18n();
3const price = 29;
4</script>
5
6<template>
7  <p>{{ $t('product.price', { amount: `$${price}` }) }}</p>
8  <!-- Output: "$29 per month" -->
9</template>

Translation:

JSON
1{
2  "product": {
3    "price": "{amount} per month"
4  }
5}

Step 6: Handle Pluralization

Different languages have different plural rules. English has 2 forms (1 item, 2 items). Polish has 3. Arabic has 6!

vue-i18n handles this automatically using the pipe syntax:

JSON
1{
2  "product": {
3    "itemCount": "No items | {n} item | {n} items"
4  }
5}

In your component:

VUE
1<script setup>
2const { t } = useI18n();
3const count = ref(0);
4</script>
5
6<template>
7  <p>{{ $t('product.itemCount', count) }}</p>
8  <!-- count=0: "No items" -->
9  <!-- count=1: "1 item" -->
10  <!-- count=5: "5 items" -->
11</template>

For complex plurals, use ICU message format:

JSON
1{
2  "cart": {
3    "summary": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}"
4  }
5}

Step 7: Format Dates, Numbers, Currencies

vue-i18n includes Intl formatters:

VUE
1<script setup>
2import { useI18n } from 'vue-i18n';
3
4const { t, n, d } = useI18n();
5const price = 1299.99;
6const releaseDate = new Date('2026-01-15');
7</script>
8
9<template>
10  <!-- Number formatting -->
11  <p>{{ n(price, 'currency') }}</p>
12  <!-- en: "$1,299.99" -->
13  <!-- es: "1.299,99 €" -->
14
15  <!-- Date formatting -->
16  <p>{{ d(releaseDate, 'long') }}</p>
17  <!-- en: "January 15, 2026" -->
18  <!-- es: "15 de enero de 2026" -->
19</template>

Configure formats in i18n.ts:

TypeScript
1export const i18n = createI18n({
2  // ... other options
3  numberFormats: {
4    en: {
5      currency: {
6        style: 'currency',
7        currency: 'USD',
8      },
9    },
10    es: {
11      currency: {
12        style: 'currency',
13        currency: 'EUR',
14      },
15    },
16  },
17  datetimeFormats: {
18    en: {
19      short: { year: 'numeric', month: 'short', day: 'numeric' },
20      long: { year: 'numeric', month: 'long', day: 'numeric' },
21    },
22    es: {
23      short: { year: 'numeric', month: 'short', day: 'numeric' },
24      long: { year: 'numeric', month: 'long', day: 'numeric' },
25    },
26  },
27});

Step 8: Switch Languages

Create a language switcher component:

VUE
1<script setup>
2import { useI18n } from 'vue-i18n';
3
4const { locale, availableLocales } = useI18n();
5
6const changeLanguage = (lang: string) => {
7  locale.value = lang;
8  // Persist to localStorage
9  localStorage.setItem('user-locale', lang);
10};
11</script>
12
13<template>
14  <div class="language-switcher">
15    <button
16      v-for="lang in availableLocales"
17      :key="lang"
18      :class="{ active: locale === lang }"
19      @click="changeLanguage(lang)"
20    >
21      {{ lang.toUpperCase() }}
22    </button>
23  </div>
24</template>

Detect user's preferred language on app load:

TypeScript
1// In i18n.ts
2function getInitialLocale(): string {
3  // 1. Check localStorage
4  const saved = localStorage.getItem('user-locale');
5  if (saved) return saved;
6
7  // 2. Check browser language
8  const browserLang = navigator.language.split('-')[0];
9  if (['en', 'es', 'fr'].includes(browserLang)) {
10    return browserLang;
11  }
12
13  // 3. Default
14  return 'en';
15}
16
17export const i18n = createI18n({
18  locale: getInitialLocale(),
19  // ...
20});

Step 9: Lazy Load Translations

Loading all languages upfront increases your bundle size. Lazy load languages on demand:

TypeScript
1// i18n.ts
2import { createI18n } from 'vue-i18n';
3
4export const i18n = createI18n({
5  legacy: false,
6  locale: 'en',
7  fallbackLocale: 'en',
8  messages: {
9    en: {}, // Start empty
10  },
11});
12
13// Load locale dynamically
14export async function loadLocale(locale: string) {
15  // Check if already loaded
16  if (i18n.global.availableLocales.includes(locale)) {
17    return;
18  }
19
20  // Lazy load the locale file
21  const messages = await import(`./locales/${locale}.json`);
22  i18n.global.setLocaleMessage(locale, messages.default);
23}
24
25// Switch language with lazy loading
26export async function setLocale(locale: string) {
27  await loadLocale(locale);
28  i18n.global.locale.value = locale;
29  localStorage.setItem('user-locale', locale);
30}

Usage:

VUE
1<script setup>
2import { setLocale } from '@/i18n';
3
4const switchLanguage = async (lang: string) => {
5  await setLocale(lang);
6};
7</script>

Result: Only English loads initially. Spanish/French load when user selects them.


Step 10: SEO-Friendly URLs with Vue Router

For SEO, use locale-based URLs:

  • example.com/en/products
  • example.com/es/productos

Configure Vue Router:

TypeScript
1import { createRouter, createWebHistory } from 'vue-router';
2import { setLocale } from './i18n';
3
4const routes = [
5  {
6    path: '/:locale',
7    component: () => import('./layouts/LocaleLayout.vue'),
8    children: [
9      { path: '', name: 'home', component: () => import('./views/Home.vue') },
10      { path: 'products', name: 'products', component: () => import('./views/Products.vue') },
11      { path: 'about', name: 'about', component: () => import('./views/About.vue') },
12    ],
13  },
14  // Redirect root to default locale
15  { path: '/', redirect: '/en' },
16];
17
18const router = createRouter({
19  history: createWebHistory(),
20  routes,
21});
22
23// Update locale on route change
24router.beforeEach(async (to) => {
25  const locale = to.params.locale as string;
26  if (locale && ['en', 'es', 'fr'].includes(locale)) {
27    await setLocale(locale);
28  }
29});
30
31export default router;

LocaleLayout.vue:

VUE
1<script setup>
2import { useRoute } from 'vue-router';
3import { watchEffect } from 'vue';
4import { useI18n } from 'vue-i18n';
5
6const route = useRoute();
7const { locale } = useI18n();
8
9// Sync locale from URL
10watchEffect(() => {
11  const urlLocale = route.params.locale as string;
12  if (urlLocale && locale.value !== urlLocale) {
13    locale.value = urlLocale;
14  }
15});
16</script>
17
18<template>
19  <div>
20    <router-view />
21  </div>
22</template>

Add hreflang tags for SEO:

TypeScript
1// In your router or layout
2const addHreflangTags = (locale: string) => {
3  const head = document.querySelector('head');
4  const existingTags = head?.querySelectorAll('link[rel="alternate"]');
5  existingTags?.forEach(tag => tag.remove());
6
7  const locales = ['en', 'es', 'fr'];
8  locales.forEach(lang => {
9    const link = document.createElement('link');
10    link.rel = 'alternate';
11    link.hreflang = lang;
12    link.href = `https://example.com/${lang}${route.path}`;
13    head?.appendChild(link);
14  });
15};

Step 11: TypeScript Support

Get autocomplete for translation keys:

TypeScript
1// types/i18n.d.ts
2import en from '@/locales/en.json';
3
4type MessageSchema = typeof en;
5
6declare module 'vue-i18n' {
7  export interface DefineLocaleMessage extends MessageSchema {}
8}

Now TypeScript will error if you use t('invalid.key'):

TypeScript
const { t } = useI18n();
t('nav.home'); // ✅ Valid
t('nav.invalid'); // ❌ TypeScript error

Step 12: Production Optimization

1. Code Splitting

Split translations by route:

TypeScript
1// Instead of loading all keys
2const messages = await import('./locales/en.json');
3
4// Load only what you need
5const common = await import('./locales/common/en.json');
6const products = await import('./locales/products/en.json');
7
8i18n.global.setLocaleMessage('en', {
9  ...common.default,
10  ...products.default,
11});

2. Preload Next Language

If most users switch from English to Spanish, preload Spanish:

TypeScript
1// Preload likely next language
2if (locale.value === 'en') {
3  setTimeout(() => loadLocale('es'), 2000);
4}

3. Compile Messages

vue-i18n can precompile messages for faster runtime:

Terminal
npm install @intlify/unplugin-vue-i18n -D

vite.config.ts:

TypeScript
1import { defineConfig } from 'vite';
2import vue from '@vitejs/plugin-vue';
3import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
4import { resolve } from 'path';
5
6export default defineConfig({
7  plugins: [
8    vue(),
9    VueI18nPlugin({
10      include: resolve(__dirname, './src/locales/**'),
11      compositionOnly: true,
12    }),
13  ],
14});

Step 13: Connect to IntlPull

Manually managing JSON files doesn't scale. Use a Translation Management System (TMS):

Install IntlPull CLI:

Terminal
npm install -D @intlpullhq/cli

Configure:

JSON
1// .intlpull.json
2{
3  "projectId": "your-project-id",
4  "sourceLanguage": "en",
5  "targetLanguages": ["es", "fr", "de"],
6  "format": "json",
7  "outputDir": "src/locales"
8}

Workflow:

  1. Upload your source translations:
Terminal
npx @intlpullhq/cli upload
  1. Push to IntlPull for translation:
Terminal
npx @intlpullhq/cli upload
  1. Download translations:
Terminal
npx @intlpullhq/cli download
  1. Auto-sync in CI/CD:
YAML
1# .github/workflows/i18n.yml
2name: Sync Translations
3on:
4  schedule:
5    - cron: '0 */6 * * *' # Every 6 hours
6
7jobs:
8  sync:
9    runs-on: ubuntu-latest
10    steps:
11      - uses: actions/checkout@v3
12      - run: npm install
13      - run: npx @intlpullhq/cli download
14      - run: |
15          git config user.name "i18n-bot"
16          git add src/locales
17          git commit -m "chore: sync translations" || exit 0
18          git push

Benefits:

  • ✅ Translators work in a UI, not JSON
  • ✅ Translation memory saves costs
  • ✅ AI pre-translation for new keys
  • ✅ Approval workflows for quality
  • ✅ Zero developer involvement after setup

Common Pitfalls

1. Forgetting to Await Lazy Loads

TypeScript
1// ❌ Bad - locale may not be loaded
2locale.value = 'es';
3
4// ✅ Good - wait for it
5await setLocale('es');

2. Hardcoded Strings in Components

Run a lint check:

Terminal
npx eslint . --rule 'no-literal-string: error'

Or use IntlPull's status check:

Terminal
npx @intlpullhq/cli status

3. Not Handling Missing Keys

Always set a fallback:

TypeScript
1createI18n({
2  fallbackLocale: 'en',
3  missing: (locale, key) => {
4    console.warn(`Missing translation: ${key} in ${locale}`);
5    return key; // Show key instead of blank
6  },
7});

4. Ignoring RTL Languages

If you support Arabic/Hebrew, add RTL CSS:

VUE
1<template>
2  <div :dir="locale === 'ar' || locale === 'he' ? 'rtl' : 'ltr'">
3    <!-- Your app -->
4  </div>
5</template>

Nuxt 3 Bonus

If you're using Nuxt 3, use @nuxtjs/i18n:

Terminal
npm install @nuxtjs/i18n@next

nuxt.config.ts:

TypeScript
1export default defineNuxtConfig({
2  modules: ['@nuxtjs/i18n'],
3  i18n: {
4    locales: [
5      { code: 'en', file: 'en.json' },
6      { code: 'es', file: 'es.json' },
7      { code: 'fr', file: 'fr.json' },
8    ],
9    defaultLocale: 'en',
10    strategy: 'prefix', // URLs: /en/products, /es/productos
11    langDir: 'locales/',
12    lazy: true, // Lazy load translations
13    detectBrowserLanguage: {
14      useCookie: true,
15      cookieKey: 'i18n_redirected',
16    },
17  },
18});

Usage in components:

VUE
<script setup>
const { t, locale, setLocale } = useI18n();
</script>

Same API as vue-i18n, but with SSR, automatic routing, and SEO built-in.



Frequently Asked Questions

What is the best i18n library for Vue 3?

vue-i18n is the standard choice for Vue 3. It supports the Composition API, TypeScript, and has excellent performance. For Nuxt projects, @nuxtjs/i18n is recommended as it wraps vue-i18n with additional features like automatic route generation and SEO meta tags.

How do I handle pluralization in Vue i18n?

Use pipe syntax or ICU format. Simple plurals use pipes: "car | cars". For complex rules (like in Polish or Arabic), use the ICU format supported by vue-i18n: "{n, plural, one {item} other {items}}". This ensures your app follows the correct grammatical rules for every language.

How do I lazy load translations in Vue?

Use dynamic imports with setLocaleMessage. Instead of importing all JSON files at the start, create a loadLocale function that uses import(./locales/${locale}.json). Call i18n.global.setLocaleMessage(locale, messages) once loaded. This significantly reduces your initial bundle size.

How do I handle SEO with Vue i18n?

Use distinct URLs for each locale. Configure Vue Router to use dynamic segments like /:locale/about. This allows search engines to index each language version separately. Additionally, inject hreflang tags into the <head> using @vueuse/head or Nuxt's built-in useHead to tell Google about alternative language versions.


Performance Benchmarks

Bundle size comparison (production build):

SetupInitial BundleAfter Lazy Load
All locales eager450 KB450 KB
Lazy load locales180 KB230 KB (when needed)
Improvement60% smaller~50% smaller

Runtime performance:

  • Translation lookup: < 0.1ms
  • Language switch: < 5ms (cached), < 100ms (lazy load)
  • Negligible impact on app performance

Checklist: Is Your Vue App i18n-Ready?

  • ✅ All user-facing strings use t() function
  • ✅ No string concatenation (use interpolation)
  • ✅ Plurals use pipe syntax or ICU format
  • ✅ Dates/numbers use d() and n() formatters
  • ✅ Translations lazy-loaded per route
  • ✅ SEO: hreflang tags + locale URLs
  • ✅ TypeScript types for autocomplete
  • ✅ Fallback locale configured
  • ✅ CI/CD syncs translations automatically
  • ✅ TMS (IntlPull) integrated for scale

Next Steps

You now have a production-ready Vue localization setup. Here's what to do next:

  1. Add more languages: Just create new JSON files and add to availableLocales
  2. Set up IntlPull: Automate translation workflow
  3. Monitor coverage: Use IntlPull's analytics to track translation progress
  4. Optimize bundle: Code-split by route if translations > 100KB
  5. A/B test: Measure conversion rates per language

Want to skip the manual work? Try IntlPull's Vue starter template. It comes with vue-i18n pre-configured, CI/CD workflows, and OTA updates.


Further Reading

Building multilingual Vue apps doesn't have to be painful. With vue-i18n and the right workflow, you can ship global products faster.

Questions? Join our Discord or contact us.

Tags
vue
vue-i18n
localization
internationalization
vue3
composition-api
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.