Internationalization (i18n) Guide
Add comprehensive multi-language support to your God Panel application with internationalization.
Overview
Internationalization (i18n) allows your application to support multiple languages and locales, making it accessible to users worldwide.
Setup
1. Install i18n Module
bash
npm install @nuxtjs/i18n2. Configure Nuxt i18n
typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@nuxtjs/i18n'
],
i18n: {
// Base URL
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
// Supported locales
locales: [
{
code: 'en',
name: 'English',
file: 'en.json',
dir: 'ltr'
},
{
code: 'fa',
name: 'فارسی',
file: 'fa.json',
dir: 'rtl'
},
{
code: 'es',
name: 'Español',
file: 'es.json',
dir: 'ltr'
}
],
// Default locale
defaultLocale: 'en',
// Strategy for URL structure
strategy: 'prefix_except_default',
// Fallback locale
fallbackLocale: 'en',
// Custom routes
pages: {
'about': {
en: '/about',
fa: '/درباره',
es: '/acerca-de'
}
},
// SEO options
seo: true,
// Lazy loading
lazy: true,
// Language detection
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root'
}
}
})3. Create Translation Files
Create translation files in the i18n/locales/ directory:
i18n/locales/en.json:
json
{
"welcome": "Welcome",
"home": {
"title": "Home",
"description": "Welcome to our application"
},
"nav": {
"dashboard": "Dashboard",
"users": "Users",
"settings": "Settings",
"profile": "Profile",
"logout": "Logout"
},
"auth": {
"login": "Login",
"register": "Register",
"email": "Email",
"password": "Password",
"confirmPassword": "Confirm Password",
"forgotPassword": "Forgot Password?",
"rememberMe": "Remember me",
"loginButton": "Sign In",
"registerButton": "Create Account"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"search": "Search",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Information"
},
"validation": {
"required": "This field is required",
"email": "Please enter a valid email address",
"minLength": "Must be at least {min} characters",
"maxLength": "Must be no more than {max} characters",
"passwordMismatch": "Passwords do not match"
}
}i18n/locales/fa.json:
json
{
"welcome": "خوش آمدید",
"home": {
"title": "خانه",
"description": "به اپلیکیشن ما خوش آمدید"
},
"nav": {
"dashboard": "داشبورد",
"users": "کاربران",
"settings": "تنظیمات",
"profile": "پروفایل",
"logout": "خروج"
},
"auth": {
"login": "ورود",
"register": "ثبت نام",
"email": "ایمیل",
"password": "رمز عبور",
"confirmPassword": "تکرار رمز عبور",
"forgotPassword": "رمز عبور را فراموش کردهاید؟",
"rememberMe": "مرا به خاطر بسپار",
"loginButton": "ورود",
"registerButton": "ایجاد حساب"
},
"common": {
"save": "ذخیره",
"cancel": "لغو",
"delete": "حذف",
"edit": "ویرایش",
"add": "افزودن",
"search": "جستجو",
"loading": "در حال بارگذاری...",
"error": "خطا",
"success": "موفقیت",
"warning": "هشدار",
"info": "اطلاعات"
},
"validation": {
"required": "این فیلد الزامی است",
"email": "لطفاً یک آدرس ایمیل معتبر وارد کنید",
"minLength": "باید حداقل {min} کاراکتر باشد",
"maxLength": "نباید بیش از {max} کاراکتر باشد",
"passwordMismatch": "رمزهای عبور مطابقت ندارند"
}
}i18n/locales/es.json:
json
{
"welcome": "Bienvenido",
"home": {
"title": "Inicio",
"description": "Bienvenido a nuestra aplicación"
},
"nav": {
"dashboard": "Panel",
"users": "Usuarios",
"settings": "Configuración",
"profile": "Perfil",
"logout": "Cerrar Sesión"
},
"auth": {
"login": "Iniciar Sesión",
"register": "Registrarse",
"email": "Correo Electrónico",
"password": "Contraseña",
"confirmPassword": "Confirmar Contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?",
"rememberMe": "Recordarme",
"loginButton": "Iniciar Sesión",
"registerButton": "Crear Cuenta"
},
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar",
"add": "Agregar",
"search": "Buscar",
"loading": "Cargando...",
"error": "Error",
"success": "Éxito",
"warning": "Advertencia",
"info": "Información"
},
"validation": {
"required": "Este campo es obligatorio",
"email": "Por favor ingresa una dirección de correo válida",
"minLength": "Debe tener al menos {min} caracteres",
"maxLength": "No debe tener más de {max} caracteres",
"passwordMismatch": "Las contraseñas no coinciden"
}
}Basic Usage
In Templates
vue
<template>
<div>
<h1>{{ $t('welcome') }}</h1>
<p>{{ $t('home.description') }}</p>
<!-- With interpolation -->
<p>{{ $t('validation.minLength', { min: 8 }) }}</p>
<!-- Pluralization -->
<p>{{ $t('items', count) }}</p>
</div>
</template>
<script setup>
// Access current locale
const { locale } = useI18n()
// Switch locale
const switchToPersian = () => {
await navigateTo('/', { locale: 'fa' })
}
</script>In Components
vue
<!-- components/LocaleSwitcher.vue -->
<template>
<div class="locale-switcher">
<button
v-for="locale in availableLocales"
:key="locale.code"
@click="switchLocale(locale.code)"
:class="{ active: locale.code === currentLocale }"
class="locale-btn"
>
<Icon :name="locale.icon" />
{{ locale.name }}
</button>
</div>
</template>
<script setup>
const { locale: currentLocale, locales } = useI18n()
const availableLocales = computed(() =>
locales.value.map(loc => ({
...loc,
icon: getLocaleIcon(loc.code)
}))
)
const getLocaleIcon = (code: string) => {
const icons: Record<string, string> = {
en: 'mdi-alpha-e',
fa: 'mdi-alpha-f',
es: 'mdi-alpha-e'
}
return icons[code] || 'mdi-translate'
}
const switchLocale = async (localeCode: string) => {
await navigateTo($route.path, { locale: localeCode })
}
</script>
<style scoped>
.locale-switcher {
display: flex;
gap: var(--space-2);
}
.locale-btn {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
background: none;
cursor: pointer;
font-size: var(--text-sm);
transition: all 0.2s ease;
}
.locale-btn:hover {
background-color: var(--bg-secondary);
}
.locale-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
</style>In Composables
typescript
// composables/useTranslation.ts
export const useTranslation = () => {
const { t, locale } = useI18n()
const translateWithFallback = (key: string, fallback: string): string => {
try {
return t(key)
} catch {
return fallback
}
}
const formatMessage = (key: string, params: Record<string, any> = {}): string => {
let message = t(key)
// Replace parameters
Object.entries(params).forEach(([param, value]) => {
message = message.replace(`{${param}}`, String(value))
})
return message
}
const getLocalizedRoute = (path: string, targetLocale?: string): string => {
const target = targetLocale || locale.value
return `/${target}${path === '/' ? '' : path}`
}
return {
t,
locale,
translateWithFallback,
formatMessage,
getLocalizedRoute
}
}Advanced Features
RTL Support
vue
<!-- layouts/rtl.vue -->
<template>
<div dir="rtl" class="rtl-layout">
<slot />
</div>
</template>
<style scoped>
.rtl-layout {
direction: rtl;
text-align: right;
}
/* RTL specific styles */
[dir="rtl"] .nav-item {
text-align: right;
}
[dir="rtl"] .icon-left {
transform: scaleX(-1);
}
[dir="rtl"] .dropdown-menu {
left: 0;
right: auto;
}
</style>Date and Number Formatting
typescript
// composables/useLocale.ts
export const useLocale = () => {
const { locale } = useI18n()
const formatDate = (date: Date | string, options?: Intl.DateTimeFormatOptions) => {
const dateObj = typeof date === 'string' ? new Date(date) : date
return new Intl.DateTimeFormat(locale.value, {
year: 'numeric',
month: 'long',
day: 'numeric',
...options
}).format(dateObj)
}
const formatNumber = (number: number, options?: Intl.NumberFormatOptions) => {
return new Intl.NumberFormat(locale.value, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
...options
}).format(number)
}
const formatCurrency = (amount: number, currency: string = 'USD') => {
return new Intl.NumberFormat(locale.value, {
style: 'currency',
currency,
minimumFractionDigits: 2
}).format(amount)
}
const formatRelativeTime = (date: Date | string) => {
const dateObj = typeof date === 'string' ? new Date(date) : date
const now = new Date()
const diffInSeconds = Math.floor((now.getTime() - dateObj.getTime()) / 1000)
const rtf = new Intl.RelativeTimeFormat(locale.value, {
numeric: 'auto'
})
if (diffInSeconds < 60) {
return rtf.format(-diffInSeconds, 'second')
} else if (diffInSeconds < 3600) {
return rtf.format(-Math.floor(diffInSeconds / 60), 'minute')
} else if (diffInSeconds < 86400) {
return rtf.format(-Math.floor(diffInSeconds / 3600), 'hour')
} else {
return rtf.format(-Math.floor(diffInSeconds / 86400), 'day')
}
}
return {
formatDate,
formatNumber,
formatCurrency,
formatRelativeTime
}
}Lazy Loading Translations
typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
lazy: true,
langDir: 'locales/',
defaultLocale: 'en',
// Lazy loading strategy
strategy: 'prefix_except_default',
// Custom loader
customRoutes: [
{
prefix: 'docs/',
pages: {
'about': {
en: '/docs/about',
fa: '/docs/درباره',
es: '/docs/acerca-de'
}
}
}
]
}
})SEO and Meta Tags
Dynamic Meta Tags
vue
<!-- pages/about.vue -->
<template>
<div>
<h1>{{ $t('about.title') }}</h1>
<p>{{ $t('about.description') }}</p>
</div>
</template>
<script setup>
// Set meta tags dynamically
const { t } = useI18n()
useHead({
title: t('about.title'),
meta: [
{
name: 'description',
content: t('about.description')
},
{
property: 'og:title',
content: t('about.title')
},
{
property: 'og:description',
content: t('about.description')
},
{
property: 'og:locale',
content: t('meta.locale')
}
]
})
</script>Language-Specific URLs
typescript
// composables/useLocalizedRoutes.ts
export const useLocalizedRoutes = () => {
const { locale, locales } = useI18n()
const getLocalizedPath = (path: string, targetLocale?: string): string => {
const target = targetLocale || locale.value
const defaultLocale = 'en'
if (target === defaultLocale) {
return path
}
return `/${target}${path}`
}
const getAlternateLinks = (path: string) => {
return locales.value.map(loc => ({
rel: 'alternate',
hreflang: loc.code,
href: `${process.env.BASE_URL}${getLocalizedPath(path, loc.code)}`
}))
}
const switchLocale = async (newLocale: string) => {
const newPath = getLocalizedPath($route.path, newLocale)
await navigateTo(newPath)
}
return {
getLocalizedPath,
getAlternateLinks,
switchLocale
}
}Translation Management
Translation Keys Structure
json
{
"nav": {
"dashboard": "Dashboard",
"users": "Users",
"settings": "Settings"
},
"auth": {
"login": {
"title": "Login",
"button": "Sign In",
"forgot": "Forgot password?"
}
},
"forms": {
"labels": {
"email": "Email address",
"password": "Password"
},
"errors": {
"required": "This field is required",
"email": "Invalid email format"
}
}
}Missing Translation Handler
typescript
// plugins/i18n.client.ts
export default defineNuxtPlugin(() => {
const { t } = useI18n()
// Custom missing translation handler
const originalT = t
const customT = (key: string, ...params: any[]) => {
const translation = originalT(key, ...params)
// Check if translation is missing (fallback to key)
if (translation === key) {
console.warn(`Missing translation for key: ${key}`)
// You could also send this to a translation service
// reportMissingTranslation(key)
}
return translation
}
return {
provide: {
t: customT
}
}
})Testing i18n
Unit Tests
typescript
// test/i18n.test.ts
describe('Internationalization', () => {
test('should translate text correctly', () => {
const { t } = useI18n()
expect(t('welcome')).toBe('Welcome')
})
test('should switch locales correctly', async () => {
const { locale, locales } = useI18n()
expect(locale.value).toBe('en')
// Switch to Persian
await navigateTo('/', { locale: 'fa' })
expect(locale.value).toBe('fa')
expect(t('welcome')).toBe('خوش آمدید')
})
test('should format dates correctly for locale', () => {
const { formatDate } = useLocale()
const date = new Date('2025-01-15')
// Test English formatting
expect(formatDate(date, { locale: 'en' }))
.toBe('January 15, 2025')
// Test Persian formatting
expect(formatDate(date, { locale: 'fa' }))
.toBe('۱۵ ژانویه ۲۰۲۵')
})
})Performance Optimization
Code Splitting
typescript
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
lazy: true,
langDir: 'locales/',
// Only load used translations
compilation: {
strictMessage: false
}
},
// Optimize bundle size
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
i18n: ['@nuxtjs/i18n']
}
}
}
}
}
})Translation Caching
typescript
// composables/useTranslationCache.ts
export const useTranslationCache = () => {
const cache = new Map<string, string>()
const getCachedTranslation = (key: string, locale: string): string | null => {
const cacheKey = `${locale}:${key}`
return cache.get(cacheKey) || null
}
const setCachedTranslation = (key: string, locale: string, translation: string) => {
const cacheKey = `${locale}:${key}`
cache.set(cacheKey, translation)
}
const clearCache = () => {
cache.clear()
}
return {
getCachedTranslation,
setCachedTranslation,
clearCache
}
}Deployment Considerations
CDN Translation Loading
typescript
// For CDN deployment, load translations dynamically
export const loadTranslations = async (locale: string) => {
try {
const translations = await $fetch(`/locales/${locale}.json`)
return translations
} catch (error) {
console.error(`Failed to load translations for ${locale}:`, error)
return {}
}
}Environment Variables
env
# i18n Configuration
NUXT_I18N_DEFAULT_LOCALE=en
NUXT_I18N_FALLBACK_LOCALE=en
NUXT_I18N_BASE_URL=https://your-domain.com
NUXT_I18N_DEBUG=falseBest Practices
Translation Key Naming
typescript
// ✅ Good
const keys = {
'nav.dashboard': 'Dashboard',
'nav.users': 'Users',
'auth.login.title': 'Sign In',
'auth.login.button': 'Sign In',
'forms.email.label': 'Email Address',
'forms.email.error.required': 'Email is required',
'forms.email.error.invalid': 'Invalid email format'
}
// ❌ Avoid
const keys = {
'dashboard': 'Dashboard',
'login': 'Sign In', // Ambiguous
'email': 'Email Address' // Context missing
}Pluralization
json
{
"items": "no items | {count} item | {count} items",
"person": "no people | {count} person | {count} people"
}Context-Aware Translations
json
{
"save": "Save",
"save.context.file": "Save File",
"save.context.settings": "Save Settings",
"save.context.draft": "Save Draft"
}Troubleshooting
Common Issues
Translations not loading:
typescript
// Check if translation files exist
const { t } = useI18n()
console.log('Available locales:', useI18n().locales.value)
console.log('Translation test:', t('welcome'))RTL layout issues:
css
/* Ensure proper RTL support */
[dir="rtl"] {
text-align: right;
}
[dir="rtl"] .flex {
flex-direction: row-reverse;
}
[dir="rtl"] .space-x-4 > * + * {
margin-right: 1rem;
margin-left: 0;
}SEO meta tags not updating:
vue
<script setup>
const { t, locale } = useI18n()
useHead({
title: t('page.title'),
htmlAttrs: {
lang: locale.value,
dir: t('meta.direction')
},
meta: [
{ name: 'description', content: t('page.description') }
]
})
</script>Migration from Other i18n Solutions
From Vue I18n
typescript
// If migrating from vue-i18n
import { createI18n } from 'vue-i18n'
export default createI18n({
legacy: false,
globalInjection: true,
locale: 'en',
messages: {
en: { /* translations */ },
fa: { /* translations */ }
}
})From Nuxt i18n v7
typescript
// Migration to v8
export default defineNuxtConfig({
i18n: {
// v8 uses 'locales' instead of 'langDir'
locales: [
{ code: 'en', file: 'en.json' },
{ code: 'fa', file: 'fa.json' }
],
// Strategy changes
strategy: 'prefix_except_default',
// SEO is now enabled by default
seo: true
}
})Resources
Next: Check out the Component Library to see how components work with internationalization!