Tutorials Vue-elingual: Teaching Your App to Speak Multiple Languages
9 min read
Loading...
It is said that: If you talk to a man in a language he understands, that goes to his head. If you talk to him in his language, that goes to his heart. ~ Nelson Mandela. Software is and should always be accessible, thus the interfaces created so that it can be used in any language. There are various approaches to this, the primary and most recognized approaches are internationalization (i18n) and localization (l10n) - they aren’t the same but are used interchangeably. For this deep dive, we are going to focus on internationalization.
The easiest way to explain or define this term is this: internationalization enables a piece of software to handle multiple language environments, localization enables a piece of software to support a specific regional language environment.
Overview
This guide will show you how to implement internationalization in your Vue 3 application using vue-i18n. You’ll learn how to structure translations, handle dynamic content, and manage language switching - all within the Vue 3 composition API context and all explained in true developer style.
1. Basic Setup and Simple Translations

<!-- App.vue -->
<template>
<div>
<h1>{{ t('welcome') }}</h1>
<p v-if="isLoading">{{ t('loading') }}</p>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
// i18n.js
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
locale: 'en', // default language
fallbackLocale: 'en', // fallback for when things go wrong
messages: {
en: {
welcome: 'Welcome to the Matrix',
errors: {
404: 'Page is hiding in another castle',
500: 'Server is having a moment'
},
loading: 'Convincing electrons to do work...'
},
es: {
welcome: 'Bienvenido a la Matrix',
errors: {
404: 'La página está escondida en otro castillo',
500: 'El servidor está teniendo un momento'
},
loading: 'Convenciendo a los electrones de trabajar...'
}
}
})
export default i18n
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './i18n'
const app = createApp(App)
app.use(i18n)
app.mount('#app')
2. Language Switching in Vue 3: The Polyglot Button

<!-- LocaleSwitcher.vue -->
<template>
<div class="lang-switcher">
<select v-model="locale" @change="switchLanguage">
<option
v-for="(name, lang) in languages"
:key="lang"
:value="lang"
>
{{ name }}
</option>
</select>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale, t } = useI18n()
const languages = {
en: "🇬🇧 English (Compile-time)",
es: "🇪🇸 Español (Tiempo de compilación)"
}
const switchLanguage = () => {
console.log(t('debug.langSwitch', { lang: locale.value }))
}
</script>
<style scoped>
.lang-switcher {
margin: 1rem 0;
}
.lang-switcher select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
</style>
3. Dynamic Values and Interpolation: When Your App Gets Personal

<!-- DynamicContent.vue -->
<template>
<div>
<p>{{ t('greetings.morning', { name: developerName }) }}</p>
<p>{{ t('bugs.count', { count: bugCount }) }}</p>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
const { t } = useI18n()
const developerName = ref('Code Wizard')
const bugCount = ref(42)
</script>
// translations/messages.js
export default {
en: {
greetings: {
morning: "Good morning {name}, have you tried turning it off and on again?",
night: "Still coding at {time}? Same here!",
weekend: "It's {day}! Time to debug in pajamas!"
},
bugs: {
count: "You have {count} bugs to fix"
},
debug: {
langSwitch: "Language switched to {lang}"
}
},
es: {
greetings: {
morning: "Buenos días {name}, ¿has intentado apagarlo y encenderlo de nuevo?",
night: "¿Aún programando a las {time}? ¡Yo también!",
weekend: "¡Es {day}! ¡Hora de depurar en pijama!"
},
bugs: {
count: "Tienes {count} errores que arreglar"
},
debug: {
langSwitch: "Idioma cambiado a {lang}"
}
}
}
4. Handling Pluralization: Because Zero is Special

<!-- PluralExample.vue -->
<template>
<div>
<p>{{ $tc('debug.bugs', bugCount, { count: bugCount }) }}</p>
<p>{{ $tc('coffee.cups', coffeeCount, { count: coffeeCount }) }}</p>
<div class="controls">
<button @click="bugCount++">Add Bug</button>
<button @click="bugCount = Math.max(0, bugCount - 1)">Fix Bug</button>
<button @click="coffeeCount++">More Coffee</button>
<button @click="coffeeCount = Math.max(0, coffeeCount - 1)">Drink Coffee</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const bugCount = ref(0)
const coffeeCount = ref(3)
</script>
<style scoped>
.controls {
margin-top: 1rem;
}
.controls button {
margin: 0.25rem;
padding: 0.5rem 1rem;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.controls button:hover {
background: #005a9e;
}
</style>
// translations/plural.js
export default {
en: {
debug: {
bugs: "No bugs found (suspicious...) | Found one bug (there's definitely more) | Found {count} bugs (and counting...)"
},
coffee: {
cups: "Emergency: No coffee remaining! | Last coffee warning! | {count} cups of coffee remaining"
}
},
es: {
debug: {
bugs: "No se encontraron errores (sospechoso...) | Se encontró un error (definitivamente hay más) | Se encontraron {count} errores (y contando...)"
},
coffee: {
cups: "¡Emergencia: No queda café! | ¡Última advertencia de café! | {count} tazas de café restantes"
}
}
}
5. Organizing Translations with Namespaces: The Developer’s Survival Kit

// translations/developer-life.js
export const developerLife = {
en: {
statusMessages: {
compiling: "Converting caffeine to code...",
debugging: "Playing hide and seek with bugs...",
deploying: "Crossing fingers and deploying...",
success: "It works! Don't touch anything!",
error: "Error: Success condition not found"
},
excuses: {
deadline: "The deadline was more of a suggestion",
bug: "It's not a bug, it's an undocumented feature",
testing: "But it works on my machine!"
}
},
es: {
statusMessages: {
compiling: "Convirtiendo cafeína en código...",
debugging: "Jugando al escondite con los errores...",
deploying: "Cruzando los dedos y desplegando...",
success: "¡Funciona! ¡No toques nada!",
error: "Error: Condición de éxito no encontrada"
},
excuses: {
deadline: "La fecha límite era más una sugerencia",
bug: "No es un error, es una característica no documentada",
testing: "¡Pero funciona en mi máquina!"
}
}
}
// translations/error-messages.js
export const errorMessages = {
en: {
errors: {
network: "Internet decided to take a coffee break",
database: "Database is practicing social distancing",
validation: "Your code is valid but my heart says no"
}
},
es: {
errors: {
network: "Internet decidió tomar un descanso de café",
database: "La base de datos está practicando distanciamiento social",
validation: "Tu código es válido pero mi corazón dice que no"
}
}
}
6. Composition API Integration: Creating a Developer-Friendly Translation Composable

<!-- TranslationExample.vue -->
<template>
<div>
<button @click="showRandomExcuse">
Generate Developer Excuse
</button>
<p v-if="currentExcuse">{{ currentExcuse }}</p>
<p>{{ debugStatus.message }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useDevTranslations } from '../composables/useDevTranslations'
const { getRandomExcuse, debugStatus } = useDevTranslations()
const currentExcuse = ref('')
const showRandomExcuse = () => {
currentExcuse.value = getRandomExcuse()
}
</script>
<style scoped>
button {
background: #42b883;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-bottom: 1rem;
}
button:hover {
background: #369870;
}
p {
margin: 0.5rem 0;
padding: 0.5rem;
background: #f8f9fa;
border-left: 4px solid #42b883;
}
</style>
// composables/useDevTranslations.js
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
export function useDevTranslations() {
const { t, locale } = useI18n()
const getRandomExcuse = () => {
const excuses = ['deadline', 'bug', 'testing']
const randomExcuse = excuses[Math.floor(Math.random() * excuses.length)]
return t(`excuses.${randomExcuse}`)
}
const getCoffeeStatus = (cups) => {
return t('coffee.cups', cups)
}
const getStatusMessage = (status) => {
return t(`statusMessages.${status}`)
}
const debugStatus = computed(() => ({
message: t('statusMessages.debugging'),
excuse: getRandomExcuse()
}))
return {
getRandomExcuse,
getCoffeeStatus,
getStatusMessage,
debugStatus,
currentLocale: locale
}
}
7. Complete Working Example

<!-- App.vue -->
<template>
<div class="app">
<header>
<LocaleSwitcher />
<h1>{{ t('title') }}</h1>
<p>{{ t('welcome') }}</p>
</header>
<main>
<DynamicContent />
<PluralExample />
<TranslationExample />
</main>
</div>
</template>
<script setup>
import LocaleSwitcher from './components/LocaleSwitcher.vue'
import DynamicContent from './components/DynamicContent.vue'
import PluralExample from './components/PluralExample.vue'
import TranslationExample from './components/TranslationExample.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style scoped>
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #eee;
}
main {
display: grid;
gap: 2rem;
}
</style>
// Complete i18n setup with all translations
import { createI18n } from 'vue-i18n'
import { developerLife } from './translations/developer-life'
import { errorMessages } from './translations/error-messages'
import messages from './translations/messages'
import plural from './translations/plural'
// Merge all translation objects
const mergedMessages = {
en: {
title: 'Vue-elingual: Multi-language Magic',
welcome: 'Welcome to the world of Vue.js internationalization!',
...messages.en,
...plural.en,
...developerLife.en,
...errorMessages.en
},
es: {
title: 'Vue-elingual: Magia Multi-idioma',
welcome: '¡Bienvenido al mundo de la internacionalización de Vue.js!',
...messages.es,
...plural.es,
...developerLife.es,
...errorMessages.es
}
}
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: mergedMessages,
legacy: false, // Use composition API mode
globalInjection: true // Allow $t in templates
})
export default i18n
Best Practices for Vue 3 i18n
1. Modular Organization
Keep translations in separate files by feature/module or sort translations like you sort your coffee cups - by importance.
2. Type Safety
Use TypeScript with vue-i18n for better type checking or because future you will thank present you.
// types/i18n.ts
export interface LocaleMessages {
welcome: string
errors: {
404: string
500: string
}
loading: string
}
3. Lazy Loading
Load language files on demand to improve initial load time or like one should brew coffee - on demand.
// Lazy loading example
const loadLocaleMessages = async (locale) => {
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
}
4. Performance Optimization
- Use the composition API for better tree-shaking
- Implement lazy loading for translations
- Consider using the
legacy: falseoption for better performance
5. State Management
Consider using Pinia to manage language preferences or because global state is like coffee - best when managed properly.
6. Testing
Write tests for your translations using Vue Test Utils or they’ll test your patience.
// Example test
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import App from './App.vue'
const i18n = createI18n({
locale: 'en',
messages: {
en: { welcome: 'Welcome' }
}
})
test('renders welcome message', () => {
const wrapper = mount(App, {
global: {
plugins: [i18n]
}
})
expect(wrapper.text()).toContain('Welcome')
})
Astro Integration Notes
When using this in an Astro project, consider:
- Component Islands: Vue components with i18n will work as Astro islands
- Build-time vs Runtime: Some translations might be better handled at build time
- SEO: Use Astro’s routing for different language versions
- Static Generation: Pre-generate pages for different locales
Additional Resources
- Vue I18n Documentation
- i18next Translation Essentials
- Vue 3 Composition API Guide
- Astro Integration Guide
Conclusion
Implementing internationalization in Vue 3 applications doesn’t have to be a nightmare. With vue-i18n and the composition API, you can create maintainable, scalable multilingual applications that speak your users’ language - literally and figuratively.
Remember: good internationalization is like good coffee - it takes time to set up properly, but once you do, everyone appreciates it.