Vue I18n Quickstart Guide

Vue.js doesn't have localization built into the framework, but lucky for us there's a simple solution - Vue I18n. Let's build a sample Vue.js and then localize it with Vue I18n.

In the interest of a Quickstart guide, we're going to assume you're using Vue.js 3.0.0+ and the latest version of Vue I18n. We're not going to dive too deep, but I'm going to show you everything you need to know to get up and running as fast as possible.

Vue I18n is a powerful, highly configurable package. I'm going to show you my preferences, but rarely is that the only way of doing things.

Install and Setup Vue and Vue I18n

Start by creating a new project with npm init vue@latest. We'll call our project i18n-example, but otherwise accept all the defaults.

➜ npm init vue@latest

Vue.js - The Progressive JavaScript Framework

✔ Project name: … i18n-example
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/jack/Code/i18n-example...

Done. Now run:

  cd i18n-example
  npm install
  npm run dev

➜

It gives us a few extra commands to run to install and compile everything, so run those. After, you'll be able to see visit http://localhost:3000/ to view your the default installation.

Next, install vue-i18n with

npm install vue-i18n@9

Our un-localized starter app

We're going to create a very simple Vue component that will demonstrate simple translations as well as trickier cases like variable interpolation and pluralization. Our app will ask a user their name, how many placeholder images they would like and give them a choice between Nicolas Cage and Steven Segal placeholder images. Then we'll display the number of images they ask for.

We're modifying the base Vue app, so we're just going to create a new component and plug it into the starter page.

Here is our component Image.vue

<script>
export default {
    data() {
        return {
            name: '',
            number: '',
            type: '',
        }
    }
}
</script>

<template>

    <div>
        <h1>What is your name?</h1>
        <input type="text" v-model="name" />
        <p v-if="name">Hello, {{ name }}!</p>

        <h1>How many placeholder images would you like?</h1>
        <input type="number" v-model="number">

        <h1>Choose your placeholder subject</h1>
        <select v-model="type">
            <option value="https://www.placecage.com/200/300">Nic Cage</option>
            <option value="https://stevensegallery.com/200/300">Steven Segal</option>
        </select>

        <div v-if="number > 0 && type">
            <p v-if="number > 1">Here are {{ number }} images from {{ type }}</p>
            <p v-else>Here is 1 image from {{ type }}</p>
            <ul>
                <li v-for="n in number"><img :src="type" /></li>
            </ul>
        </div>
    </div>
</template>

And we'll just replace the TheWelcome component from App.vue with our new Image component. That looks like this:

<script setup>
import HelloWorld from './components/HelloWorld.vue'
import Image from './components/Image.vue'
</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!," />
    </div>
  </header>

  <main>
    <Image />
  </main>
</template>
<!-- ... -->

Now when we visit our app, we see this:

Not the prettiest, but it'll do. Now let's localize.

Language files

There are many ways to store your translations with Vue I18n. I like lazy loading JSON files for the best performance and maintainability.

We're going to store our files in src/locales/ using the ISO 639-1 code and optional ISO 3166-1 country code to name them. We'll default to English and have an option for Mexican Spanish. So, our languages files are:

src/locales/en.json
src/locales/es-mx.json

en.json looks like this:

{
    "your_name": "What is your name?",
    "hello": "Hello, {name}",
    "num_placeholders": "How many placeholders images would you like?",
    "choose": "Choose your placeholder subject",
    "here_are_your_images": "Here is one image from {type}. | Here are {count} images from {type}"
}

And here is es-mx.json

{
    "your_name": "¿Cómo te llamas?",
    "hello": "Hola {name}",
    "num_placeholders": "¿Cuántas imágenes de marcadores de posición le gustaría?",
    "choose": "Elija su tema de marcador de posición",
    "here_are_your_images": "Aquí hay una imagen de {type}. | Aquí hay {count} imágenes de {type}"
}

We just used a machine translation for this, but if the were a serious web app, we would use TranslateCI for higher quality translations and to keep everything up to date.

We're doing a couple of interesting things here. We're using placeholders in curly brackets here to interpolate dynamic content later. And the here_are_your_images key is using pluralization. We separate them with vertical pipes and we'll tell Vue which one to use based on the situation in the section on displaying our translated phrases.

Lazy Loading Our Translation Files

The Vue I18n docs get us pretty far here. We're mostly using the code from their docs to lazy load our translation files. I had a little bit of trouble implementing straight from the docs, but this GitHub example repo provided some helpful hints

First, we're going to create a new file at src/i18n.js that looks like this:

import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import messages from './locales/en.json'

export const SUPPORT_LOCALES = ['en', 'es-mx']

export const i18n = createI18n({
    locale: 'en',
    fallbackLocale: 'en',
    messages: { 'en': messages }
})

export function setI18nLanguage(i18n, locale) {
  if (i18n.mode === 'legacy') {
    i18n.global.locale = locale
  } else {
    i18n.global.locale.value = locale
  }
  /**
   * NOTE:
   * If you need to specify the language setting for headers, such as the `fetch` API, set it here.
   * The following is an example for axios.
   *
   * axios.defaults.headers.common['Accept-Language'] = locale
   */
  document.querySelector('html').setAttribute('lang', locale)
}

export async function loadLocaleMessages(i18n, locale) {
  // load locale messages with dynamic import
  const messages = await import(
    /* webpackChunkName: "locale-[request]" */ `./locales/${locale}.json`
  )
  // set locale and locale message
  i18n.global.setLocaleMessage(locale, messages.default)

  return nextTick()
}

This loads our default translations file and gives us a few exports we can use in our application.

SUPPORT_LOCALES is an array of our support locales.

i18n is our i18n object we'll use when we work with locales, default to English.

setI18nLanguage sets the locale attribute in i18n and the lang attribute in our HTML tag.

loadLocaleMessages takes care of importing our language files.

Then, we'll use i18n in our main.js file:

import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './i18n'

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

Now let's update our component to use Vue I18n.

Updating Our Component

Next we're going to update the Image.vue component to use Vue I18n and the translation strings we defined earlier. It looks like this:

<script>
export default {
    data() {
        return {
            name: '',
            number: '',
            type: '',
        }
    }
}
</script>

<template>
    <div>
        <h1>{{ $t('your_name') }}</h1>
        <input type="text" v-model="name" />
        <p v-if="name">{{ $t('hello', { name: 'Jack' }) }}</p>

        <h1>{{ $t('num_placeholders') }}</h1>
        <input type="number" v-model="number">

        <h1>{{ $t('choose') }}</h1>
        <select v-model="type">
            <option value="https://www.placecage.com/200/300">Nic Cage</option>
            <option value="https://stevensegallery.com/200/300">Steven Segal</option>
        </select>

        <div v-if="number > 0 && type">
            {{ $tc('here_are_your_images', { count: number, type: type}) }}
            <ul>
                <li v-for="n in number"><img :src="type" /></li>
            </ul>
        </div>
    </div>
</template>

Pretty simple, we're just using two functions that Vue I18n provides: $t and $tc.

$t is used for non-pluralized strings. It takes an optional second argument to define any placeholders we put in our language files.

$tc is used for pluralization. The second argument defines any placeholders and a count parameter that determines which phrase to show. Note though that there are many different language-specific pluralization rules. Vue I18n defaults to zero, one or other but you can define other pluralization rules if you need to. Check out the docs for more info if you need it.

And, if we refresh the page, it looks exactly the same as it did before. BUT, if we manually switch the locale to es-mx by editing main.js:

import { createApp } from 'vue'
import App from './App.vue'
import { i18n, loadLocaleMessages, setI18nLanguage } from './i18n'

createApp(App).use(i18n).mount('#app')
loadLocaleMessages(i18n, 'es-mx')
setI18nLanguage(i18n, 'es-mx')

And now when we refresh:

Our Image component in SpanishRevert main.js and we'll create a locale switcher component.

Creating a Locale Switcher Component

Let's create a very simple locale switcher for our application. This will be a simple dropdown that allows the user to select their language. Create a new file at components/LocaleSwitcher.vue that looks like this:

<script>
import { loadLocaleMessages, setI18nLanguage, i18n } from '../i18n'
export default {
    data() {
        return{
            locale: 'en',
        }
    },
    methods: {
        switchLocale() {
            loadLocaleMessages(i18n, this.locale)
            setI18nLanguage(i18n, this.locale)
        }
    }
}
</script>

<template>
    <div>
        <select @change="switchLocale()" v-model="locale">
            <option value="en">English</option>
            <option value="es-mx">Spanish (Mexico)</option>
        </select>
    </div>
</template>

Note you would want to add more validation to make sure the user is submitting a valid locale.

Now, we'll just include this in our Image component:

<script>
import LocaleSwitcher from './LocaleSwitcher.vue'
export default {
  components: { LocaleSwitcher },
    data() {
        return {
            name: '',
            number: '',
            type: '',
        }
    }
}
</script>

<template>
    <div>
        <locale-switcher />
        <h1>{{ $t('your_name') }}</h1>
        <input type="text" v-model="name" />
<!-- ... -->

And we have a working locale switcher!

Where to go from here

Now that you're able to server your application in different languages, it's time to hire translators, export your language files to spreadsheets and make sure everything gets updated whenever you push new features.

OR

You could give TranslateCI a try and make it easy on yourself.