Skip to content

Multi-language support

  1. Install the Internationalization plugin for Vue
ps
npm install vue-i18n
npm install @intlify/unplugin-vue-i18n
  1. Add plugin to ./vite.config.ts
ts
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import path from 'node:path'

// ...
export default defineConfig({
  plugins: [
    // ... other plugins
    VueI18nPlugin({
      include: path.resolve(__dirname, './src/i18n/**')
    }),
  ],
})
  1. Create some initial localization files.
@/i18n/en.json - / EN file
json
{
  "welcome": "Welcome",
  "logout": "Logout",
  "login": "Login",
  "app.actions.login": "App actions login",
  "app.actions.logout": "App actions logout",
  "app.messages.welcome": "App messages welcome",
  "Request failed with status code 403": "Request failed with status code 403",
  "you.are.not.allowed.to.access.this.page": "You are not allowed to access this page",
  "copied.to.clipboard": "Copied to clipboard"
}
@/i18n/en.json - / FR file
json
{
  "welcome": "Bienvenue",
  "logout": "\"Déconnexion\"",
  "login": "Connexion",
  "app.actions.login": "\"Actions d'application connexion\"",
  "app.actions.logout": "\"Actions de l'application déconnexion\"",
  "app.messages.welcome": "\"Messages d'application bienvenus\"",
  "Request failed with status code 403": "La requête a échoué avec le code d'état 403",
  "you.are.not.allowed.to.access.this.page": "Vous n'êtes pas autorisé à accéder à cette page",
  "copied.to.clipboard": "Copié dans le presse-papiers"
}
@/i18n/sandbox/en.json - /sandbox EN file
json
{
  "sandbox.title": "Sandbox title",
  "sandbox.content": "Sandbox content",
  "sandbox.missing": "Sandbox missing",
  "View": "View",
  "": "",
  "Delete": "Delete",
  "This is a snack message": "This is a snack message",
  "close": "Close",
  "unauthorized": "Unauthorized",
  "Number is required": "Number is required",
  "Text 3 is required": "Text 3 is required",
  "validate": "Validate",
  "cancel": "Cancel",
  "reset": "Reset",
  "custom": "Custom",
  "Text 1": "Text 1",
  "Enter some text": "Enter some text",
  "Text 2": "Text 2",
  "Text 3": "Text 3",
  "Email": "Email",
  "Enter valid e-mail address": "Enter valid e-mail address",
  "Number": "Number",
  "Enter some number": "Enter some number",
  "Enter password, at least 12 characters": "Enter password, at least 12 characters",
  "Textarea": "Textarea",
  "Enter some larger text here": "Enter some larger text here",
  "Accept Terms and Conditions": "Accept Terms and Conditions",
  "Rating": "Rating",
  "Checkbox": "Checkbox",
  "Select": "Select",
  "Combo": "Combo",
  "Autocomplete": "Autocomplete",
  "File!": "File!",
  "Date": "Date",
  "Select date": "Select date",
  "Time": "Time",
  "Select time": "Select time",
  "Date and Time": "Date and Time",
  "Select date and time": "Select date and time",
  "Error from outside": "Error from outside",
  "Text 2 is required": "Text 2 is required",
  "Text 2 must be a valid JSON": "Text 2 must be a valid JSON",
  "Text 3 must contain \"custom\"": "Text 3 must contain \"custom\"",
  "Number must be at least 10": "Number must be at least 10"
}
@/i18n/sandbox/fr.json - /sandbox FR file
json
{
  "sandbox.title": "\"Titre du bac à sable\"",
  "sandbox.content": "\"Contenu du bac à sable\"",
  "sandbox.missing": "\"Bac à sable manquant\"",
  "View": "Vue",
  "": "You didn't write any text that needs translation. Could you please provide the text?",
  "Delete": "Supprimer",
  "This is a snack message": "\"C'est un message de collation\"",
  "close": "Fermer",
  "unauthorized": "Non autorisé",
  "validate": "Valider",
  "cancel": "Annuler",
  "reset": "Réinitialiser",
  "custom": "Personnalisé",
  "Enter some text": "Saisissez du texte",
  "Text 1": "Texte 1",
  "Text 2": "Texte 2",
  "Text 3": "Texte 3",
  "Email": "E-mail",
  "Enter valid e-mail address": "Entrez une adresse e-mail valide",
  "Number": "Nombre",
  "Enter some number": "Saisissez un nombre",
  "Enter password, at least 12 characters": "Entrez un mot de passe d'au moins 12 caractères",
  "Textarea": "Zone de texte",
  "Enter some larger text here": "Entrez un texte plus long ici",
  "Accept Terms and Conditions": "Acceptez les termes et conditions",
  "Rating": "Évaluation",
  "Checkbox": "Case à cocher",
  "Select": "Sélectionner",
  "Combo": "Combo",
  "Autocomplete": "Autocomplétion",
  "File!": "Fichier!",
  "Date": "Date",
  "Select date": "Sélectionnez une date",
  "Time": "Temps",
  "Select time": "Sélectionnez une heure",
  "Date and Time": "Date et heure",
  "Select date and time": "Sélectionnez une date et une heure",
  "Error from outside": "Erreur externe",
  "Text 2 is required": "Texte 2 est requis",
  "Text 3 is required": "Texte 3 est requis",
  "Number is required": "Nombre est requis"
}

Translation File Structure:

src/i18n/
├── en.json          # Base English translations
├── fr.json          # Base French translations
└── moduleA/         # Module-specific translations
    ├── en.json
    └── fr.json

⚠️ translation structure can be only one level deep, so everything under moduleA has to be included in either base or module translation files.

  1. Create i18n configuration @/plugins/i18n.ts
ts
import { createI18n } from 'vue-i18n'
import messages from '@intlify/unplugin-vue-i18n/messages'

const i18n = createI18n({
  legacy: false,
  globalInjection: true,
  locale: 'en',
  fallbackLocale: 'en',
  fallbackWarn: false,
  messages,
})

export default i18n
  1. Initialize i18n in main.ts
ts
// ...
import i18n from './plugins/i18n'
// ...
app.use(i18n)
// ...
  1. Test

Modify @/App.vue

vue
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
function toggleLocale() {
  locale.value = locale.value === 'en' ? 'fr' : 'en'
}
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <nav>
        <v-btn @click="toggleLocale()">{{ locale }}</v-btn>
        <RouterLink to="/">{{ t('app.home') }}</RouterLink>
        <RouterLink to="/about">{{ t('app.about') }}</RouterLink>
        <RouterLink to="/sandbox">Sandbox</RouterLink>
      </nav>
    </div>
  </header>
  <RouterView />
</template>

And @/pages/sandbox/index.vue

vue
<template>
  <v-card :style="useCardBackground('#00AA00')">
    <v-card-title><v-icon icon="$mdiHome" />{{ t('sandbox.title') }}</v-card-title>
    <v-card-text>{{ t('sandbox.content') }}</v-card-text>
    <!-- ... -->
  </v-card>
</template>

<script setup lang="ts">
// ...
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// ...
</script>