I’ve always believed that sharing the “why” and “how” behind a product is just as important as building it. When I decided to add a blog to ClarityBox a gentle journaling tool for processing conversations and emotions—I wanted to ensure every piece of content was structured, performant, and discoverable. Internationalization (i18n) is crucial for making your Vue applications accessible to a global audience. In this guide, we’ll walk through implementing i18n in a Vue 3 application using TypeScript, with features like lazy loading and locale persistence.
Project Structure
First, let’s understand how to organize our i18n implementation:
src/
├── locales/ # Translation files
│ ├── en.json # English translations
│ └── fr.json # French translations
├── components/ # Vue components
│ └── LanguageSwitcher.vue
├── i18n.ts # i18n configuration
└── main.ts # Vue app entry point
Setting Up i18n Configuration
Let’s start by creating our i18n configuration file:
// filepath: f:\claritybox-landing-page-vue\src\i18n.ts
import { createI18n } from 'vue-i18n'
// Type for messages
type MessageSchema = {
[key: string]: string | MessageSchema
}
// Type for supported locales
type SupportedLocales = 'en' | 'fr'
const i18n = createI18n({
legacy: false, // Use Composition API
locale: 'en', // Default locale
fallbackLocale: 'en',
messages: {}, // Start with empty messages
})
// Async locale loading
export async function loadLocaleMessages(locale: SupportedLocales) {
if (!i18n.global.availableLocales.includes(locale)) {
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
}
i18n.global.locale.value = locale
}
// Locale switcher with lazy loading
export async function setLocale(locale: SupportedLocales) {
try {
await loadLocaleMessages(locale)
localStorage.setItem('user-locale', locale)
document.documentElement.setAttribute('lang', locale)
} catch (error) {
console.error(`Failed to load locale "${locale}":`, error)
}
}
export default i18n
Translation Files
Create your translation files in the locales directory:
// filepath: \src\locales\en.json
{
"welcome": "Welcome",
"nav": {
"home": "Home",
"about": "About"
}
}
// filepath: \src\locales\fr.json
{
"welcome": "Bienvenue",
"nav": {
"home": "Accueil",
"about": "À propos"
}
}
Language Switcher Component
Create a component to handle language switching:
// filepath: \src\components\LanguageSwitcher.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { setLocale } from '../i18n'
const { locale } = useI18n()
const loading = ref(false)
const switchLocale = async (newLocale: 'en' | 'fr') => {
if (locale.value === newLocale) return
loading.value = true
try {
await setLocale(newLocale)
} finally {
loading.value = false
}
}
</script>
<template>
<div class="flex items-center gap-2">
<button
@click="switchLocale('en')"
:class="[
'px-2 py-1 rounded',
locale === 'en'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
]"
:disabled="loading"
>
EN
</button>
<button
@click="switchLocale('fr')"
:class="[
'px-2 py-1 rounded',
locale === 'fr'
? 'bg-primary-100 text-primary-700'
: 'text-gray-600 hover:bg-gray-100'
]"
:disabled="loading"
>
FR
</button>
</div>
</template>
Using Translations
Here’s how to use translations in your components:
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<h1>{{ t('welcome') }}</h1>
<nav>
<a href="/">{{ t('nav.home') }}</a>
<a href="/about">{{ t('nav.about') }}</a>
</nav>
</template>
Key Features and Benefits
- Lazy Loading: Translations are loaded only when needed
- Type Safety: Full TypeScript support
- Persistence: User’s language preference is saved
- Fallback: Uses English as fallback for missing translations
- Accessibility: Automatically updates HTML lang attribute
- Modern API: Uses Vue 3 Composition API
- Performance: Minimal initial bundle size
This implementation provides a solid foundation for internationalizing your Vue 3 application. The lazy loading approach ensures optimal performance, while TypeScript integration provides type safety and better developer experience.
Remember to properly structure your translation files and keep them organized as your application grows. The component-based approach makes it easy to add language switching functionality anywhere in your application.