vue-i18n — Internationalization for Vue.js Applications
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
- 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.
- Not setting fallbackLocale. Missing translations in the current locale show the key name. Always set a fallback locale that has complete translations.
- Loading all locale files upfront. For apps with 5+ languages, lazy load locale messages to avoid shipping 100KB+ of unused translations on initial load.
- 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.
- 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
- How do you set up vue-i18n with the Vue 3 composition API?
- What is the difference between t, tc, d, and n in vue-i18n?
- How do you lazy load locale messages in vue-i18n?
- How does vue-i18n handle pluralization for languages with multiple plural forms?
- 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
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