Skip to content

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/i18n

2. 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=false

Best 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!

Released under the MIT License.