i18next-vue

Last updated: July 31, 2025Author: Jakub Pomykała

Installation

Install vue-i18next and its dependencies:

npm install vue-i18next i18next i18next-vue

Project Setup

Vue 3 Configuration

Create the i18next configuration:

// src/i18n/index.ts
import i18next from 'i18next'
import I18NextVue from 'i18next-vue'
import { App } from 'vue'

// Import translation files
import en from './locales/en.json'
import pl from './locales/pl.json'
import es from './locales/es.json'
import fr from './locales/fr.json'

export const SUPPORT_LOCALES = ['en', 'pl', 'es', 'fr']

export async function setupI18n(app: App) {
  await i18next.init({
    lng: localStorage.getItem('selectedLanguage') || 'en',
    fallbackLng: 'en',
    debug: process.env.NODE_ENV === 'development',

    resources: {
      en: { translation: en },
      pl: { translation: pl },
      es: { translation: es },
      fr: { translation: fr }
    },

    interpolation: {
      escapeValue: false // Vue already does escaping
    },

    react: {
      useSuspense: false
    }
  })

  app.use(I18NextVue, { i18next })
  return i18next
}

Main Application Setup

// src/main.ts
import { createApp } from 'vue'
import { setupI18n } from './i18n'
import App from './App.vue'

async function initApp() {
  const app = createApp(App)

  await setupI18n(app)

  app.mount('#app')
}

initApp()

Translation File Structure

Create translation files in the src/i18n/locales/ directory:

// src/i18n/locales/en.json
{
  "homepage": {
    "title": "Welcome to our website",
    "description": "This is a multilingual Vue.js application",
    "subtitle": "Built with vue-i18next"
  },
  "navigation": {
    "home": "Home",
    "about": "About",
    "contact": "Contact",
    "products": "Products"
  },
  "product": {
    "name": "Product Name",
    "price": "Price: {{price}}",
    "available": "Available since: {{date}}",
    "addToCart": "Add to Cart",
    "inStock": "In Stock",
    "outOfStock": "Out of Stock",
    "description": "Product description with <strong>HTML</strong> content"
  },
  "cart": {
    "title": "Shopping Cart",
    "empty": "Your cart is empty",
    "total": "Total: {{amount}}",
    "checkout": "Checkout",
    "items_zero": "No items",
    "items_one": "{{count}} item",
    "items_other": "{{count}} items"
  },
  "user": {
    "welcome": "Welcome, {{name}}!",
    "profile": "Profile",
    "logout": "Logout",
    "login": "Login",
    "greeting": "Hello {{name}}, you have {{count}} message",
    "greeting_plural": "Hello {{name}}, you have {{count}} messages"
  },
  "forms": {
    "email": "Email",
    "password": "Password",
    "submit": "Submit",
    "cancel": "Cancel",
    "save": "Save",
    "required": "This field is required",
    "validation": {
      "email": "Please enter a valid email",
      "minLength": "Minimum {{count}} characters required"
    }
  },
  "messages": {
    "loading": "Loading...",
    "error": "Something went wrong",
    "success": "Operation completed successfully",
    "confirm": "Are you sure?",
    "networkError": "Network error occurred"
  },
  "datetime": {
    "now": "just now",
    "minuteAgo": "{{count}} minute ago",
    "minuteAgo_plural": "{{count}} minutes ago",
    "hourAgo": "{{count}} hour ago",
    "hourAgo_plural": "{{count}} hours ago"
  }
}
// src/i18n/locales/pl.json
{
  "homepage": {
    "title": "Witamy na naszej stronie",
    "description": "To jest wielojęzyczna aplikacja Vue.js",
    "subtitle": "Zbudowana z vue-i18next"
  },
  "navigation": {
    "home": "Strona główna",
    "about": "O nas",
    "contact": "Kontakt",
    "products": "Produkty"
  },
  "product": {
    "name": "Nazwa produktu",
    "price": "Cena: {{price}}",
    "available": "Dostępne od: {{date}}",
    "addToCart": "Dodaj do koszyka",
    "inStock": "Na stanie",
    "outOfStock": "Brak na stanie",
    "description": "Opis produktu z <strong>HTML</strong> treścią"
  },
  "cart": {
    "title": "Koszyk",
    "empty": "Twój koszyk jest pusty",
    "total": "Razem: {{amount}}",
    "checkout": "Złóż zamówienie",
    "items_zero": "Brak przedmiotów",
    "items_one": "{{count}} przedmiot",
    "items_few": "{{count}} przedmioty",
    "items_many": "{{count}} przedmiotów",
    "items_other": "{{count}} przedmiotów"
  },
  "user": {
    "welcome": "Witaj, {{name}}!",
    "profile": "Profil",
    "logout": "Wyloguj",
    "login": "Zaloguj",
    "greeting": "Cześć {{name}}, masz {{count}} wiadomość",
    "greeting_few": "Cześć {{name}}, masz {{count}} wiadomości",
    "greeting_many": "Cześć {{name}}, masz {{count}} wiadomości",
    "greeting_other": "Cześć {{name}}, masz {{count}} wiadomości"
  },
  "forms": {
    "email": "Email",
    "password": "Hasło",
    "submit": "Wyślij",
    "cancel": "Anuluj",
    "save": "Zapisz",
    "required": "To pole jest wymagane",
    "validation": {
      "email": "Wprowadź prawidłowy email",
      "minLength": "Wymagane minimum {{count}} znaków"
    }
  },
  "messages": {
    "loading": "Ładowanie...",
    "error": "Coś poszło nie tak",
    "success": "Operacja zakończona pomyślnie",
    "confirm": "Czy jesteś pewny?",
    "networkError": "Wystąpił błąd sieci"
  },
  "datetime": {
    "now": "przed chwilą",
    "minuteAgo": "{{count}} minutę temu",
    "minuteAgo_few": "{{count}} minuty temu",
    "minuteAgo_many": "{{count}} minut temu",
    "minuteAgo_other": "{{count}} minut temu",
    "hourAgo": "{{count}} godzinę temu",
    "hourAgo_few": "{{count}} godziny temu",
    "hourAgo_many": "{{count}} godzin temu",
    "hourAgo_other": "{{count}} godzin temu"
  }
}
// src/i18n/locales/es.json
{
  "homepage": {
    "title": "Bienvenido a nuestro sitio web",
    "description": "Esta es una aplicación Vue.js multiidioma",
    "subtitle": "Construida con vue-i18next"
  },
  "navigation": {
    "home": "Inicio",
    "about": "Acerca de",
    "contact": "Contacto",
    "products": "Productos"
  },
  "product": {
    "name": "Nombre del producto",
    "price": "Precio: {{price}}",
    "available": "Disponible desde: {{date}}",
    "addToCart": "Añadir al carrito",
    "inStock": "En stock",
    "outOfStock": "Agotado",
    "description": "Descripción del producto con contenido <strong>HTML</strong>"
  },
  "cart": {
    "title": "Carrito de compras",
    "empty": "Tu carrito está vacío",
    "total": "Total: {{amount}}",
    "checkout": "Finalizar compra",
    "items_zero": "Sin artículos",
    "items_one": "{{count}} artículo",
    "items_other": "{{count}} artículos"
  },
  "user": {
    "welcome": "¡Bienvenido, {{name}}!",
    "profile": "Perfil",
    "logout": "Cerrar sesión",
    "login": "Iniciar sesión",
    "greeting": "Hola {{name}}, tienes {{count}} mensaje",
    "greeting_plural": "Hola {{name}}, tienes {{count}} mensajes"
  },
  "forms": {
    "email": "Correo electrónico",
    "password": "Contraseña",
    "submit": "Enviar",
    "cancel": "Cancelar",
    "save": "Guardar",
    "required": "Este campo es obligatorio",
    "validation": {
      "email": "Introduce un email válido",
      "minLength": "Se requieren mínimo {{count}} caracteres"
    }
  },
  "messages": {
    "loading": "Cargando...",
    "error": "Algo salió mal",
    "success": "Operación completada exitosamente",
    "confirm": "¿Estás seguro?",
    "networkError": "Ocurrió un error de red"
  },
  "datetime": {
    "now": "ahora mismo",
    "minuteAgo": "hace {{count}} minuto",
    "minuteAgo_plural": "hace {{count}} minutos",
    "hourAgo": "hace {{count}} hora",
    "hourAgo_plural": "hace {{count}} horas"
  }
}
// src/i18n/locales/fr.json
{
  "homepage": {
    "title": "Bienvenue sur notre site web",
    "description": "Ceci est une application Vue.js multilingue",
    "subtitle": "Construite avec vue-i18next"
  },
  "navigation": {
    "home": "Accueil",
    "about": "À propos",
    "contact": "Contact",
    "products": "Produits"
  },
  "product": {
    "name": "Nom du produit",
    "price": "Prix: {{price}}",
    "available": "Disponible depuis: {{date}}",
    "addToCart": "Ajouter au panier",
    "inStock": "En stock",
    "outOfStock": "Rupture de stock",
    "description": "Description du produit avec contenu <strong>HTML</strong>"
  },
  "cart": {
    "title": "Panier",
    "empty": "Votre panier est vide",
    "total": "Total: {{amount}}",
    "checkout": "Commander",
    "items_zero": "Aucun article",
    "items_one": "{{count}} article",
    "items_other": "{{count}} articles"
  },
  "user": {
    "welcome": "Bienvenue, {{name}} !",
    "profile": "Profil",
    "logout": "Déconnexion",
    "login": "Connexion",
    "greeting": "Salut {{name}}, vous avez {{count}} message",
    "greeting_plural": "Salut {{name}}, vous avez {{count}} messages"
  },
  "forms": {
    "email": "Email",
    "password": "Mot de passe",
    "submit": "Soumettre",
    "cancel": "Annuler",
    "save": "Sauvegarder",
    "required": "Ce champ est requis",
    "validation": {
      "email": "Veuillez entrer un email valide",
      "minLength": "Minimum {{count}} caractères requis"
    }
  },
  "messages": {
    "loading": "Chargement...",
    "error": "Quelque chose s'est mal passé",
    "success": "Opération terminée avec succès",
    "confirm": "Êtes-vous sûr ?",
    "networkError": "Erreur réseau survenue"
  },
  "datetime": {
    "now": "à l'instant",
    "minuteAgo": "il y a {{count}} minute",
    "minuteAgo_plural": "il y a {{count}} minutes",
    "hourAgo": "il y a {{count}} heure",
    "hourAgo_plural": "il y a {{count}} heures"
  }
}

Usage Examples

Basic Component Usage (Composition API)

<!-- HomePage.vue -->
<template>
  <div class="homepage">
    <h1>{{ $t('homepage.title') }}</h1>
    <p>{{ $t('homepage.description') }}</p>
    <small>{{ $t('homepage.subtitle') }}</small>

    <LanguageSwitcher />
    <ProductShowcase :products="products" />
    <UserGreeting :user="currentUser" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18next'
import LanguageSwitcher from './LanguageSwitcher.vue'
import ProductShowcase from './ProductShowcase.vue'
import UserGreeting from './UserGreeting.vue'

const { t } = useI18n()

const products = ref([
  {
    id: 1,
    name: 'Sample Product',
    price: 29.99,
    availableDate: new Date('2023-01-15'),
    inStock: true
  }
])

const currentUser = ref({
  name: 'John Doe',
  messageCount: 3
})
</script>

Language Switcher Component

<!-- LanguageSwitcher.vue -->
<template>
  <div class="language-switcher">
    <label for="language-select">{{ $t('ui.selectLanguage') }}:</label>
    <select
      id="language-select"
      :value="currentLanguage"
      @change="changeLanguage">
      <option
        v-for="lang in availableLanguages"
        :key="lang.code"
        :value="lang.code">
        {{ lang.name }}
      </option>
    </select>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18next'

const { i18n } = useI18n()

const availableLanguages = [
  { code: 'en', name: 'English' },
  { code: 'pl', name: 'Polski' },
  { code: 'es', name: 'Español' },
  { code: 'fr', name: 'Français' }
]

const currentLanguage = computed(() => i18n.language)

const changeLanguage = async (event: Event) => {
  const target = event.target as HTMLSelectElement
  const newLanguage = target.value

  await i18n.changeLanguage(newLanguage)
  localStorage.setItem('selectedLanguage', newLanguage)
}
</script>

Product Component with Complex Translations

<!-- ProductCard.vue -->
<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>{{ $t('product.price', { price: formatPrice(product.price) }) }}</p>
    <p>{{ $t('product.available', { date: formatDate(product.availableDate) }) }}</p>

    <!-- HTML content in translations -->
    <div v-html="$t('product.description')"></div>

    <div class="product-status">
      <span :class="stockClass">
        {{ product.inStock ? $t('product.inStock') : $t('product.outOfStock') }}
      </span>
    </div>

    <button
      @click="addToCart"
      :disabled="!product.inStock"
      class="btn btn-primary">
      {{ $t('product.addToCart') }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18next'

interface Product {
  id: number
  name: string
  price: number
  availableDate: Date
  inStock: boolean
}

const props = defineProps<{
  product: Product
}>()

const { t, i18n } = useI18n()

const stockClass = computed(() => ({
  'in-stock': props.product.inStock,
  'out-of-stock': !props.product.inStock
}))

const formatPrice = (price: number) => {
  return new Intl.NumberFormat(i18n.language, {
    style: 'currency',
    currency: 'USD'
  }).format(price)
}

const formatDate = (date: Date) => {
  return new Intl.DateTimeFormat(i18n.language, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date)
}

const addToCart = () => {
  console.log('Added to cart:', props.product.name)
}
</script>

User Greeting with Pluralization

<!-- UserGreeting.vue -->
<template>
  <div class="user-greeting">
    <h2>{{ $t('user.welcome', { name: user.name }) }}</h2>

    <!-- Using i18next pluralization -->
    <p>{{ $t('user.greeting', { name: user.name, count: user.messageCount }) }}</p>

    <!-- Manual pluralization with conditions -->
    <p v-if="user.messageCount === 0">{{ $t('messages.noMessages') }}</p>
    <p v-else-if="user.messageCount === 1">{{ $t('messages.oneMessage') }}</p>
    <p v-else>{{ $t('messages.multipleMessages', { count: user.messageCount }) }}</p>

    <div class="user-actions">
      <button @click="viewProfile" class="btn btn-secondary">
        {{ $t('user.profile') }}
      </button>
      <button @click="logout" class="btn btn-outline">
        {{ $t('user.logout') }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18next'

interface User {
  name: string
  messageCount: number
}

const props = defineProps<{
  user: User
}>()

const { t } = useI18n()

const viewProfile = () => {
  console.log('Viewing profile for:', props.user.name)
}

const logout = () => {
  console.log('Logging out:', props.user.name)
}
</script>

Shopping Cart with Pluralization

<!-- ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <h2>{{ $t('cart.title') }}</h2>

    <div v-if="items.length === 0" class="empty-cart">
      {{ $t('cart.empty') }}
    </div>

    <div v-else class="cart-content">
      <div class="cart-summary">
        <!-- Using i18next pluralization with _zero, _one, _other -->
        {{ $t('cart.items', { count: items.length }) }}
      </div>

      <div class="cart-items">
        <div v-for="item in items" :key="item.id" class="cart-item">
          <span>{{ item.name }}</span>
          <span>{{ formatPrice(item.price) }}</span>
        </div>
      </div>

      <div class="cart-total">
        {{ $t('cart.total', { amount: formatPrice(getTotal()) }) }}
      </div>

      <button @click="checkout" class="btn btn-primary">
        {{ $t('cart.checkout') }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18next'

interface CartItem {
  id: number
  name: string
  price: number
}

const props = defineProps<{
  items: CartItem[]
}>()

const { t, i18n } = useI18n()

const getTotal = () => {
  return props.items.reduce((total, item) => total + item.price, 0)
}

const formatPrice = (price: number) => {
  return new Intl.NumberFormat(i18n.language, {
    style: 'currency',
    currency: 'USD'
  }).format(price)
}

const checkout = () => {
  console.log('Proceeding to checkout')
}
</script>

Translation Composable

// src/composables/useTranslations.ts
import { computed } from 'vue'
import { useI18n } from 'vue-i18next'

export function useTranslations() {
  const { t, i18n } = useI18n()

  const currentLanguage = computed(() => i18n.language)

  const translate = (key: string, options?: any) => {
    return t(key, options)
  }

  const changeLanguage = async (language: string) => {
    await i18n.changeLanguage(language)
    localStorage.setItem('selectedLanguage', language)
  }

  const formatCurrency = (amount: number, currency = 'USD') => {
    return new Intl.NumberFormat(i18n.language, {
      style: 'currency',
      currency
    }).format(amount)
  }

  const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions) => {
    return new Intl.DateTimeFormat(i18n.language, options).format(date)
  }

  const formatRelativeTime = (date: Date) => {
    const now = new Date()
    const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)

    if (diffInSeconds < 60) {
      return t('datetime.now')
    } else if (diffInSeconds < 3600) {
      const minutes = Math.floor(diffInSeconds / 60)
      return t('datetime.minuteAgo', { count: minutes })
    } else {
      const hours = Math.floor(diffInSeconds / 3600)
      return t('datetime.hourAgo', { count: hours })
    }
  }

  return {
    currentLanguage,
    translate,
    changeLanguage,
    formatCurrency,
    formatDate,
    formatRelativeTime
  }
}

SimpleLocalize Configuration

To manage translations with SimpleLocalize, you need to set up the CLI tool.

# macOS / Linux / Windows (WSL) curl -s https://get.simplelocalize.io/2.9/install | bash # Windows (PowerShell) . { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex; # npm npm install @simplelocalize/cli
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.9/install | bash

# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex;

# npm
npm install @simplelocalize/cli

Create simplelocalize.yml in your project root:

apiKey: YOUR_PROJECT_API_KEY

uploadFormat: single-language-json
uploadPath: ./src/i18n/locales/{lang}.json
uploadLanguageKey: en
uploadOptions:
  - REPLACE_TRANSLATION_IF_FOUND

downloadPath: ./src/i18n/locales/{lang}.json
downloadLanguageKeys: ['en', 'pl', 'es', 'fr']
downloadFormat: single-language-json

Work with the CLI to manage your translations:

# Upload translations to SimpleLocalize
simplelocalize upload

# Download translations for all languages
simplelocalize download

# Build your Vue.js application
npm run build

Key Features

  • i18next Ecosystem: Leverage the mature i18next ecosystem and plugins
  • Advanced Pluralization: Support for complex plural rules across languages
  • Interpolation: Variable interpolation with formatting options
  • Namespaces: Organize translations into logical namespaces
  • Context: Context-sensitive translations
  • HTML Support: Safe HTML rendering in translations
  • Lazy Loading: Load translations on demand
  • Change Detection: Automatic re-rendering when language changes

Best Practices

1. Use Namespaces for Organization

// Organize translations by namespace
const i18n = createI18next({
  ns: ['common', 'navigation', 'forms'],
  defaultNS: 'common',
  resources: {
    en: {
      common: { /* common translations */ },
      navigation: { /* navigation translations */ },
      forms: { /* form translations */ }
    }
  }
})

2. Handle Missing Translations

// Add missing key handler
const i18n = createI18next({
  missingKeyHandler: (lng, ns, key) => {
    console.warn(`Missing translation: ${key} for language: ${lng}`)
  }
})

3. Use Context for Variations

{
  "friend": "A friend",
  "friend_male": "A boyfriend",
  "friend_female": "A girlfriend"
}

Resources

Was this helpful?