vue-i18n

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

Installation

First, install the required dependencies in your Vue.js project:

npm install vue-i18n@9

For Vue 2 projects, use:

npm install vue-i18n@8

Vue.js Configuration

Vue 3 Setup

Create an i18n configuration file:

// src/i18n/index.ts
import { createI18n } from 'vue-i18n'

// Import your 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'

const messages = {
  en,
  pl,
  es,
  fr
}

const i18n = createI18n({
  legacy: false, // Use Composition API mode
  locale: 'en', // Default locale
  fallbackLocale: 'en',
  messages,
  globalInjection: true, // Make $t available globally
})

export default i18n

Configure your main application file:

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

const app = createApp(App)

app.use(i18n)
app.mount('#app')

Vue 2 Setup

For Vue 2 projects:

// src/i18n/index.js
import Vue from 'vue'
import VueI18n from 'docs/integrations/vuejs/vue-i18n.mdx'

Vue.use(VueI18n)

// Import your 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'

const messages = {
  en,
  pl,
  es,
  fr
}

const i18n = new VueI18n({
  locale: 'en', // Default locale
  fallbackLocale: 'en',
  messages,
})

export default i18n
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import i18n from './i18n'

new Vue({
  i18n,
  render: h => h(App),
}).$mount('#app')

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"
  },
  "product": {
    "price": "Price: {price}",
    "available": "Available since: {date}",
    "inStock": "In stock: {count} | In stock: {count} items"
  },
  "navigation": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  },
  "user": {
    "greeting": "Hello {name}!",
    "profile": "User profile"
  }
}
// src/i18n/locales/pl.json
{
  "homepage": {
    "title": "Witamy na naszej stronie",
    "description": "To jest wielojęzyczna aplikacja Vue.js"
  },
  "product": {
    "price": "Cena: {price}",
    "available": "Dostępne od: {date}",
    "inStock": "Na stanie: {count} sztuka | Na stanie: {count} sztuki | Na stanie: {count} sztuk"
  },
  "navigation": {
    "home": "Strona główna",
    "about": "O nas",
    "contact": "Kontakt"
  },
  "user": {
    "greeting": "Cześć {name}!",
    "profile": "Profil użytkownika"
  }
}
// src/i18n/locales/es.json
{
  "homepage": {
    "title": "Bienvenido a nuestro sitio web",
    "description": "Esta es una aplicación Vue.js multiidioma"
  },
  "product": {
    "price": "Precio: {price}",
    "available": "Disponible desde: {date}",
    "inStock": "En stock: {count} artículo | En stock: {count} artículos"
  },
  "navigation": {
    "home": "Inicio",
    "about": "Acerca de",
    "contact": "Contacto"
  },
  "user": {
    "greeting": "¡Hola {name}!",
    "profile": "Perfil de usuario"
  }
}
// src/i18n/locales/fr.json
{
  "homepage": {
    "title": "Bienvenue sur notre site web",
    "description": "Ceci est une application Vue.js multilingue"
  },
  "product": {
    "price": "Prix: {price}",
    "available": "Disponible depuis: {date}",
    "inStock": "En stock: {count} article | En stock: {count} articles"
  },
  "navigation": {
    "home": "Accueil",
    "about": "À propos",
    "contact": "Contact"
  },
  "user": {
    "greeting": "Bonjour {name}!",
    "profile": "Profil utilisateur"
  }
}

Usage Examples

Basic Translation (Vue 3 Composition API)

<template>
  <div>
    <h1>{{ $t('homepage.title') }}</h1>
    <p>{{ $t('homepage.description') }}</p>

    <!-- Using interpolation -->
    <p>{{ $t('user.greeting', { name: 'John' }) }}</p>
  </div>
</template>

<script setup>
import { useI18n } from 'vue-i18n'

const { t, locale } = useI18n()

// You can also use the composable directly
const greeting = computed(() => t('user.greeting', { name: 'John' }))
</script>

Basic Translation (Vue 3 Options API / Vue 2)

<template>
  <div>
    <h1>{{ $t('homepage.title') }}</h1>
    <p>{{ $t('homepage.description') }}</p>

    <!-- Using interpolation -->
    <p>{{ $t('user.greeting', { name: userName }) }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userName: 'John'
    }
  },
  computed: {
    greeting() {
      return this.$t('user.greeting', { name: this.userName })
    }
  }
}
</script>

Language Switcher Component

<template>
  <div class="language-switcher">
    <select v-model="currentLocale" @change="changeLanguage">
      <option v-for="lang in availableLanguages" :key="lang.code" :value="lang.code">
        {{ lang.name }}
      </option>
    </select>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'

const { locale, availableLocales } = useI18n()

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

const currentLocale = computed({
  get: () => locale.value,
  set: (value) => {
    locale.value = value
    // Save to localStorage for persistence
    localStorage.setItem('user-locale', value)
  }
})

const changeLanguage = () => {
  // Language change is handled by the computed setter
}
</script>

<style scoped>
.language-switcher select {
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
}
</style>

Advanced Formatting and Pluralization

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>

    <!-- Number formatting -->
    <p>{{ $t('product.price', { price: $n(product.price, 'currency') }) }}</p>

    <!-- Date formatting -->
    <p>{{ $t('product.available', { date: $d(product.availableDate, 'short') }) }}</p>

    <!-- Pluralization -->
    <p>{{ $tc('product.inStock', product.stockCount, { count: product.stockCount }) }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const product = ref({
  name: 'Example Product',
  price: 29.99,
  availableDate: new Date('2025-01-15'),
  stockCount: 5
})
</script>

Custom Number and Date Formats

// src/i18n/index.ts
import { createI18n } from 'vue-i18n'

const numberFormats = {
  en: {
    currency: {
      style: 'currency',
      currency: 'USD',
      notation: 'standard'
    },
    decimal: {
      style: 'decimal',
      minimumFractionDigits: 2,
      maximumFractionDigits: 2
    }
  },
  pl: {
    currency: {
      style: 'currency',
      currency: 'PLN',
      notation: 'standard'
    },
    decimal: {
      style: 'decimal',
      minimumFractionDigits: 2,
      maximumFractionDigits: 2
    }
  }
}

const dateTimeFormats = {
  en: {
    short: {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    },
    long: {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
      weekday: 'short',
      hour: 'numeric',
      minute: 'numeric'
    }
  },
  pl: {
    short: {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    },
    long: {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
      weekday: 'short',
      hour: 'numeric',
      minute: 'numeric'
    }
  }
}

const i18n = createI18n({
  legacy: false,
  locale: 'en',
  fallbackLocale: 'en',
  messages,
  numberFormats,
  dateTimeFormats
})

Dynamic Locale Loading

For larger applications, you might want to load translations dynamically:

// src/i18n/index.ts
import { createI18n } from 'vue-i18n'

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

export function setupI18n(options = { locale: 'en' }) {
  const i18n = createI18n({
    legacy: false,
    globalInjection: true,
    locale: options.locale,
    fallbackLocale: 'en',
    ...options
  })
  setI18nLanguage(i18n, options.locale)
  return i18n
}

export function setI18nLanguage(i18n, locale) {
  i18n.global.locale.value = locale
  document.querySelector('html').setAttribute('lang', locale)
}

export async function loadLocaleMessages(i18n, locale) {
  // Load locale messages with dynamic import
  const messages = await import(`./locales/${locale}.json`)

  // Set locale and locale message
  i18n.global.setLocaleMessage(locale, messages.default)

  return nextTick()
}
// Usage in component
import { loadLocaleMessages, setI18nLanguage } from '@/i18n'

const changeLanguage = async (locale) => {
  // Check if locale messages are already loaded
  if (!i18n.global.availableLocales.includes(locale)) {
    await loadLocaleMessages(i18n, locale)
  }
  setI18nLanguage(i18n, locale)
}

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/en.json
uploadLanguageKey: en
uploadOptions:
  - REPLACE_TRANSLATION_IF_FOUND

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

Extraction Script (Optional)

For automatic key extraction, you can create a custom script:

// scripts/extract-i18n.js
const fs = require('fs')
const path = require('path')

function extractTranslationKeys(dir) {
  const keys = new Set()
  const files = fs.readdirSync(dir)

  files.forEach(file => {
    const filePath = path.join(dir, file)
    const stat = fs.statSync(filePath)

    if (stat.isDirectory()) {
      extractTranslationKeys(filePath).forEach(key => keys.add(key))
    } else if (file.endsWith('.vue') || file.endsWith('.js') || file.endsWith('.ts')) {
      const content = fs.readFileSync(filePath, 'utf8')

      // Extract $t('key') patterns
      const tMatches = content.match(/\$t\(['"`]([^'"`]+)['"`]\)/g)
      if (tMatches) {
        tMatches.forEach(match => {
          const key = match.match(/\$t\(['"`]([^'"`]+)['"`]\)/)[1]
          keys.add(key)
        })
      }

      // Extract $tc('key') patterns
      const tcMatches = content.match(/\$tc\(['"`]([^'"`]+)['"`]/g)
      if (tcMatches) {
        tcMatches.forEach(match => {
          const key = match.match(/\$tc\(['"`]([^'"`]+)['"`]/)[1]
          keys.add(key)
        })
      }
    }
  })

  return Array.from(keys)
}

// Extract keys and create a template
const keys = extractTranslationKeys('./src')
const template = {}

keys.forEach(key => {
  const parts = key.split('.')
  let current = template

  for (let i = 0; i < parts.length - 1; i++) {
    if (!current[parts[i]]) {
      current[parts[i]] = {}
    }
    current = current[parts[i]]
  }

  current[parts[parts.length - 1]] = key
})

fs.writeFileSync('./extracted-keys.json', JSON.stringify(template, null, 2))
console.log(`Extracted ${keys.length} translation keys`)

Add the script to your package.json:

{
  "scripts": {
    "extract:i18n": "node scripts/extract-i18n.js",
    "i18n:upload": "npm run extract:i18n && simplelocalize upload",
    "i18n:download": "simplelocalize download"
  }
}

Work with the CLI to manage your translations:

# Extract translation keys from your Vue components
npm run extract:i18n

# Upload translations to SimpleLocalize
simplelocalize upload

# Download translations for all languages
simplelocalize download

Best Practices

1. Key Naming Convention

Use descriptive, hierarchical keys:

// Good
$t('navigation.menu.home')
$t('product.details.price')
$t('user.profile.settings.privacy')

// Avoid
$t('home')
$t('price')
$t('privacy')

2. Component-Scoped Translations

For component-specific translations, consider using i18n custom blocks:

<template>
  <div>
    <h1>{{ $t('title') }}</h1>
    <p>{{ $t('description') }}</p>
  </div>
</template>

<script setup>
// Component logic
</script>

<i18n>
{
  "en": {
    "title": "Component Title",
    "description": "Component description"
  },
  "pl": {
    "title": "Tytuł komponentu",
    "description": "Opis komponentu"
  }
}
</i18n>

3. Type Safety (TypeScript)

Create type definitions for your translations:

// types/i18n.ts
export interface MessageSchema {
  homepage: {
    title: string
    description: string
  }
  navigation: {
    home: string
    about: string
    contact: string
  }
  // ... other keys
}
// src/i18n/index.ts
import { createI18n } from 'vue-i18n'
import type { MessageSchema } from '@/types/i18n'

const i18n = createI18n<[MessageSchema], 'en' | 'pl' | 'es' | 'fr'>({
  legacy: false,
  locale: 'en',
  fallbackLocale: 'en',
  messages
})

Resources

Was this helpful?