Skip to content

vue-i18n — Internationalization for Vue.js Applications

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about vue. We cover key concepts, practical examples, and best practices to help you master this topic.

vue-i18n provides internationalization for Vue.js with global localization, locale messages, datetime/number formatting, and component-level translations.

What You'll Learn

By the end of this tutorial, you'll understand how to set up vue-i18n in a Vue 3 application, use the composition API with useI18n, handle pluralization and datetime formatting, and implement locale switching.

Why It Matters

Vue's reactive system requires i18n integration that responds to locale changes. vue-i18n is the official internationalization plugin for Vue, providing deep integration with Vue's reactivity, SSR support, and TypeScript types. It handles everything from simple string replacement to complex plural rules and datetime formatting.

Real-World Use

A Vue 3 e-commerce site uses vue-i18n with lazy-loaded locale messages. Each locale file is loaded on demand when the user switches language. The product catalog, checkout flow, and admin panel all reactively update. Date formats switch between "December 31, 2026" (en) and "31. Dezember 2026" (de) automatically.

Vue-i18n Architecture

graph LR
    A[Vue App] --> B[vue-i18n Plugin]
    B --> C[Locale Messages
en.json, es.json, etc.] B --> D[DateTime Formats] B --> E[Number Formats] B --> F[Fallback Locale] A --> G[Composition API
useI18n()] A --> H[Component Option
i18n] A --> I[Global Inject
$t, $tc, $d, $n] B --> J[Reactive updates
on locale change] style A fill:#4a90d9,color:#fff style B fill:#27ae60,color:#fff

Basic Setup

// i18n/index.js — Vue 3 + vue-i18n setup
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';

// Load locale messages
const messages = {
    en: {
        common: {
            welcome: 'Welcome to our store!',
            search: 'Search products...',
            cart: 'Shopping Cart',
            checkout: 'Checkout',
            items: 'No items | 1 item | {count} items'
        },
        product: {
            title: 'Product Details',
            price: 'Price: {price}',
            add_to_cart: 'Add to Cart',
            out_of_stock: 'Out of Stock'
        },
        footer: {
            copyright: 'Copyright © 2026 DodaTech',
            about: 'About Us',
            contact: 'Contact'
        }
    },
    de: {
        common: {
            welcome: 'Willkommen in unserem Shop!',
            search: 'Produkte suchen...',
            cart: 'Warenkorb',
            checkout: 'Zur Kasse',
            items: 'Keine Artikel | 1 Artikel | {count} Artikel'
        },
        product: {
            title: 'Produktdetails',
            price: 'Preis: {price}',
            add_to_cart: 'In den Warenkorb',
            out_of_stock: 'Ausverkauft'
        },
        footer: {
            copyright: 'Copyright © 2026 DodaTech',
            about: 'Uber uns',
            contact: 'Kontakt'
        }
    },
    fr: {
        common: {
            welcome: 'Bienvenue dans notre boutique !',
            search: 'Rechercher des produits...',
            cart: 'Panier',
            checkout: 'Commander',
            items: 'Aucun article | 1 article | {count} articles'
        },
        product: {
            title: 'Details du produit',
            price: 'Prix : {price}',
            add_to_cart: 'Ajouter au panier',
            out_of_stock: 'Rupture de stock'
        },
        footer: {
            copyright: 'Copyright © 2026 DodaTech',
            about: 'A propos',
            contact: 'Contact'
        }
    }
};

// Create i18n instance
const i18n = createI18n({
    locale: navigator.language.split('-')[0] || 'en',
    fallbackLocale: 'en',
    messages,
    // Enable composition API
    legacy: false,
    globalInjection: true,
});

// Register with Vue app
const app = createApp(App);
app.use(i18n);
app.mount('#app');

Composition API Usage

<!-- components/ProductCard.vue — Using useI18n composition API -->
<template>
    <div class="product-card">
        <h3>{{ t('product:title') }}</h3>
        <p class="price">{{ t('product:price', { price: formatPrice(product.price) }) }}</p>

        <button
            :disabled="!product.inStock"
            @click="addToCart"
        >
            {{ product.inStock ? t('product:add_to_cart') : t('product:out_of_stock') }}
        </button>

        <p class="stock-info">
            {{ tc('common:items', product.stockCount) }}
        </p>

        <p class="date">
            {{ d(product.releaseDate, 'long') }}
        </p>
    </div>
</template>

<script setup>
import { useI18n } from 'vue-i18n';

const { t, tc, d, n, locale } = useI18n();

const props = defineProps({
    product: Object
});

function formatPrice(amount) {
    return n(amount, {
        style: 'currency',
        currency: 'EUR',
        currencyDisplay: 'symbol'
    });
}

function addToCart() {
    // Add to cart logic
}
</script>

Locale Switcher

<!-- components/LocaleSwitcher.vue — Language switcher with reactive updates -->
<template>
    <div class="locale-switcher">
        <select
            v-model="currentLocale"
            @change="switchLocale"
            aria-label="Select language"
        >
            <option
                v-for="locale in locales"
                :key="locale.code"
                :value="locale.code"
            >
                {{ locale.nativeLabel }}
            </option>
        </select>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';

const { locale } = useI18n();

const currentLocale = ref(locale.value);

const locales = [
    { code: 'en', label: 'English', nativeLabel: 'English' },
    { code: 'de', label: 'German', nativeLabel: 'Deutsch' },
    { code: 'fr', label: 'French', nativeLabel: 'Francais' },
    { code: 'ar', label: 'Arabic', nativeLabel: 'العربية' },
];

function switchLocale(event) {
    const newLocale = event.target.value;
    locale.value = newLocale;

    // Update HTML direction for RTL
    document.documentElement.lang = newLocale;
    document.documentElement.dir = newLocale === 'ar' ? 'rtl' : 'ltr';

    // Persist preference
    localStorage.setItem('locale', newLocale);
}
</script>

Lazy Loading Locales

// i18n/lazy-setup.js — Lazy load locale messages
import { createI18n } from 'vue-i18n';

// Create i18n instance with empty messages
const i18n = createI18n({
    locale: 'en',
    fallbackLocale: 'en',
    messages: {
        en: {}  // Start with empty English — load on demand
    },
    legacy: false
});

// Lazy load function
export async function loadLocaleMessages(locale) {
    try {
        const messages = await import(`./locales/${locale}.json`);
        i18n.global.setLocaleMessage(locale, messages.default);
        i18n.global.locale.value = locale;
        console.log(`Loaded locale: ${locale}`);
    } catch (error) {
        console.error(`Failed to load locale ${locale}:`, error);

        // Fallback to English
        if (locale !== 'en') {
            await loadLocaleMessages('en');
        }
    }
}

// Usage in App.vue:
// import { loadLocaleMessages } from './i18n/lazy-setup';
// const detectedLocale = navigator.language.split('-')[0] || 'en';
// loadLocaleMessages(detectedLocale);

DateTime and Number Formatting

// i18n/index.js — DateTime and number format configuration
import { createI18n } from 'vue-i18n';

const i18n = createI18n({
    locale: 'en',
    fallbackLocale: 'en',
    messages: { /* ... */ },

    // Custom datetime formats
    datetimeFormats: {
        'en': {
            short: { year: 'numeric', month: 'short', day: 'numeric' },
            long: {
                year: 'numeric',
                month: 'long',
                day: 'numeric',
                weekday: 'long'
            },
            time: { hour: 'numeric', minute: 'numeric' }
        },
        'de': {
            short: { year: 'numeric', month: 'short', day: 'numeric' },
            long: {
                year: 'numeric',
                month: 'long',
                day: 'numeric',
                weekday: 'long'
            },
            time: { hour: '2-digit', minute: '2-digit' }
        },
        'fr': {
            short: { year: 'numeric', month: 'short', day: 'numeric' },
            long: {
                year: 'numeric',
                month: 'long',
                day: 'numeric',
                weekday: 'long'
            },
            time: { hour: '2-digit', minute: '2-digit' }
        }
    },

    // Custom number formats
    numberFormats: {
        'en': {
            currency: {
                style: 'currency',
                currency: 'USD',
                currencyDisplay: 'symbol'
            },
            decimal: {
                style: 'decimal',
                minimumFractionDigits: 2
            },
            percent: {
                style: 'percent',
                minimumFractionDigits: 1
            }
        },
        'de': {
            currency: {
                style: 'currency',
                currency: 'EUR',
                currencyDisplay: 'symbol'
            },
            decimal: {
                style: 'decimal',
                minimumFractionDigits: 2
            },
            percent: {
                style: 'percent',
                minimumFractionDigits: 1
            }
        },
        'fr': {
            currency: {
                style: 'currency',
                currency: 'EUR',
                currencyDisplay: 'symbol'
            },
            decimal: {
                style: 'decimal',
                minimumFractionDigits: 2
            },
            percent: {
                style: 'percent',
                minimumFractionDigits: 1
            }
        }
    }
});

export default i18n;

// Usage in component:
// const { d, n } = useI18n();
// d(new Date(), 'long')     // "Monday, December 31, 2026"
// d(new Date(), 'time')     // "3:00 PM" or "15:00"
// n(1234.56, 'currency')    // "$1,234.56" or "1.234,56 €"

Common Mistakes

  1. Using legacy: true with Vue 3 composition API. The legacy mode uses the Options API pattern ($t, $tc). For Vue 3 composition API, set legacy: false and use the useI18n composable.
  2. Not setting fallbackLocale. Missing translations in the current locale show the key name. Always set a fallback locale that has complete translations.
  3. Loading all locale files upfront. For apps with 5+ languages, lazy load locale messages to avoid shipping 100KB+ of unused translations on initial load.
  4. Forgetting to update dir for RTL locales. Arabic, Hebrew, and Persian need dir="rtl" on the root element. Listen to locale changes and update the dir attribute accordingly.
  5. Not handling SSR in Nuxt.js. For Nuxt apps, use the @nuxtjs/i18n module which handles server-side locale detection, SEO-friendly URLs, and Serialization of the i18n state.

Practice Questions

  1. How do you set up vue-i18n with the Vue 3 composition API?
  2. What is the difference between t, tc, d, and n in vue-i18n?
  3. How do you lazy load locale messages in vue-i18n?
  4. How does vue-i18n handle pluralization for languages with multiple plural forms?
  5. Why must the dir attribute be updated when switching to an RTL locale?

Challenge: Build a Vue 3 product catalog with vue-i18n supporting English, German, and Arabic. Implement lazy loading of locale files, a locale switcher that updates direction for RTL, datetime formatting for product release dates, currency formatting for prices, and pluralization for stock counts.

FAQ

Can I use vue-i18n with Vue 2?

Yes, but use version 8.x for Vue 2 compatibility. Vue 3 requires vue-i18n 9.x. The API is similar but not identical — check the migration guide if upgrading.

How does vue-i18n handle SSR in Nuxt?

Use the @nuxtjs/i18n module. It handles server-side locale detection, generates SEO-friendly localized routes (/en/products, /de/produkte), and ensures the server-rendered HTML matches the client's hydration.

Is vue-i18n compatible with Pinia/Vuex?

Yes. You can use Pinia or Vuex to manage the current locale globally. Set locale.value from the store and vue-i18n reactively updates all components.

How do I handle component-level translations in vue-i18n?

Use the i18n option in the component options or custom blocks in single-file components. These are merged with the global messages during component initialization.

Can I use ICU MessageFormat with vue-i18n?

Yes. vue-i18n uses its own message format syntax by default, but you can install the @intlify/vue-i18n/message-resolver to use ICU MessageFormat syntax for complex pluralization and select rules.

Mini Project

Build a Vue 3 multi-language storefront: 3 product pages with translations, locale switcher with RTL support, lazy loaded locale files, datetime formatting for product release dates, currency formatting for prices (USD for en, EUR for de/fr, SAR for ar), pluralization for stock counts, and a footer with copyright year formatting.

What's Next

You've mastered vue-i18n. Next, learn about ICU Message Format for standard syntax in translation strings across platforms.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro