How to translate your Vue.js (v2) application with vue-i18n

Andreas Löw
How to translate your Vue.js (v2) application with vue-i18n

Overview

This version of the tutorial covers Vue.js v2. We also have a newer version that covers Vue.js v3: How to translate your Vue.js app with vue-i18n

This tutorial guides you through adding translations to your Vue.js application using vue-i18n. The tutorial starts with a blank demo project which you'll create in the next step. You can of course apply the steps to your own project...

The second part of the tutorial explains how you can use BabelEdit to easily manage your translations. This includes fast pre-translation using machine translation, import and export of translations to work with translators and more.

Preparing a simple demo project

You can of course skip these steps if you've already installed vue-js...

Install vue-cli

yarn global add @vue/cli

Create the project

vue create vue-i18n-demo

Choose the default:

  • babel
  • eslint

Start the demo project:

cd vue-i18n-demo
yarn serve

Open your browser and navigate to http://localhost:8080/ - you should see a vuejs welcome screen.

Adding vue-i18n

If you use vue-cli it's easy to add vue-i18n. You need vue-i18n version 8 for this.

yarn add vue-i18n@8

Now you have to answer some questions:

  • The locale of project localization: en
  • The fallback locale of project localization: en
  • The directory where store localization messages of project. It's stored under src directory: locales
  • Enable locale messages in Single file components ? y

vue-i18n makes some changes to your project files. Let's review them. If you don't want to use vue-cli you can easily make the changes manually.

Files changed by vue-cli in your project

package.json

New dependencies:

vue-i18n

New dev-dependencies:

@kazupon/vue-i18n-loader
vue-cli-plugin-i18n

You can add these changes manually using yarn:

yarn add vue-i18n
yarn add --dev @kazupon/vue-i18n-loader vue-cli-plugin-i18n

.env

A new .env file appears in the project. It contains the default and fallback language configurations:

VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=en

vue.config.js

module.exports = {
  pluginOptions: {
    i18n: {
      locale: 'en',
      fallbackLocale: 'en',
      localeDir: 'locales',
      enableInSFC: true
    }
  }
}

src/components/HelloI18n.vue

This is a simple component that displays a simple message. It makes use of the new <i18n> section in the Single File Component. The new section contains the translation used in this file:

<template>
  <p>{{ $t('hello') }}</p>
</template>

<script>
export default {
  name: 'HelloI18n'
}
</script>

<i18n>
{
  "en": {
    "hello": "Hello i18n in SFC!"
  }
}
</i18n>

src/App.vue

App.vue requires some manual changes to render the new HelloI18n component instead of the HelloWorld component:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloI18n/>
  </div>
</template>

<script>
import HelloI18n from "./components/HelloI18n";

export default {
  name: 'app',
  components: {
    HelloI18n
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

src/i18n.js

This file imports JSON files with translations from the locales folder and initialises VueI18n:

import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

function loadLocaleMessages () {
  const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
  const messages = {}
  locales.keys().forEach(key => {
    const matched = key.match(/([A-Za-z0-9-_]+)\./i)
    if (matched && matched.length > 1) {
      const locale = matched[1]
      messages[locale] = locales(key)
    }
  })
  return messages
}

export default new VueI18n({
  locale: process.env.VUE_APP_I18N_LOCALE || 'en',
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
  messages: loadLocaleMessages()
})

src/main.js

import Vue from 'vue'
import App from './App.vue'
import i18n from './i18n'

Vue.config.productionTip = false

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

src/locales/en.json

This is a json message file containing translations for English:

{
  "message": "hello i18n !!"
}

Where to keep your translations: An important decision

The translation works like this: You have an ID for the texts that you want to present in your application, and a file contains the translations for each ID.

Vue.js offers you a bunch of options where you can keep the translations. Here are some pros and cons for each variant:

inline as data in javascript code

This is what the documentation of vue-i18n uses in its examples. The advantage is that the examples are self-contained in one file.

That's nice for a simple example — but it's a nightmare when it comes to real projects especially if you work with a translator. Those guys are quite unhappy with editing js source code — believe me! And you really don't want to correct all the syntax errors in your files when you get them back.

js files

These files contain the translations as an exported data set. It's your choice put one or multiple languages in one file.

The advantage is that these files can easily be imported using require/import. The separation of program code and translations is a step in the right direction but still not a good solution.

Vue.js single file components aka .vue files

Single file components can contain a <i18n>-section with the translation data.

While it seems to be a good idea to make the components self-contained it turns out to be a bad idea in the long run. I am discussing this issue in more detail here: Why using i18n sections in vue.js single files components is a bad idea.

My recommendation: Don't use .vue files to store your translations.

JSON files

Similar to the js files but more portable. You can still import/require them directly in your code or load them from a server.

You can easily validate JSON and YAML files and there are tools that can help you manage the translations. With an editor like BabelEdit it's impossible to create invalid files.

You can keep one, or many languages in one file — but I'd really recommend using one file per language. It's easier for the translator if he can reference other translations. This increases the consistency of your app — e.g. did you use "delete", "remove" or "discard".

My recommendation: Use JSON files if you are working on a bigger project.

So, what should I do?

I'd recommend using .vue files for small projects with portable components, .json files for bigger projects. You'll see both options during this tutorial which should give you enough knowledge to make your choice.

Adding translations

The demo project now contains both recommended methods for storing your translations:

  • the folder src/locales/*.js with a bunch of language files
  • the single file component section <i18n>

While it is possible to use both ways at the same time, I'd really recommend making a decision for one of the methods.

The main difference between the JSON files and the SFC section is that the translations in the JSON files are globally available whereas the SFC sections are only available in the component's scope.

The translation in the SFC wins if a translation is present in both, the JSON and the SFC. The SCF method is great if you want to re-use your components in different projects.

.vue files

In the HelloI18n.vue you can add more translations by extending the <i18n> section:

<i18n>
{
  "en": {
    "hello": "Hello i18n in SFC!",
    "welcome": "Welcome!",
    "yes-button": "Yes",
    "no-button": "No!"
  },
  "de": {
    "hello": "Hallo i18n in SFC!",
    "welcome": "Willkommen!",
    "yes-button": "Ja",
    "no-button": "Nein!"
  }
}
</i18n>

JSON files

If you prefer JSON add the new translations to the en.json and create a new de.json for the German translation:

src/locales/en.json

{
    "hello": "Hello i18n in SFC!",
    "welcome": "Welcome!",
    "yes-button": "Yes",
    "no-button": "No!"
}

src/locales/de.json

{
    "hello": "Hallo i18n in SFC!",
    "welcome": "Willkommen!",
    "yes-button": "Ja",
    "no-button": "Nein!"
}

Using translations in your templates

Simple text

To use a translation use the $t() function with a translation id as parameter:

<template>
  <p>{{ $t('hello') }}</p>
</template>

Text with parameters

You can of course add parameters to your translation texts:

{
    "hello": "Hallo {name}!"
}
<template>
  <p>{{ $t('hello', { name: 'Vue.js'}) }}</p>
</template>

Translating attribute values

You can pass translated attribute values to sub components using this way:

<template>
  <sub-component :title="$t('hello')"></sub-component>
</template>

Pluralization

Pluralization is about changing the text depending on the values.

Use the $tc(id,count,parameters) function to set your translation with the following parameters:

  • id id of the translation
  • count the number of items for pluralization
  • parameters parameters to add to the text (optional)

Singular and plural

Add a | to separate singular and plural:

{
    "text": "flower | flowers",
}
<template>
  <p>{{ $tc('text', 2) }}</p>
</template>

Zero, one or many

Add a | to separate singular and plural:

{
    "wolves": "no wolf | one wolf | a pack of wolves"
}
<template>
  <p>{{ $tc('wolves', 1) }}</p>
</template>

vue-i18n automatically replaces {count} and {n} in the translation with the number passed to the $tc() function:

{
    "wolves": "no wolf | one wolf | a pack of {n} wolves"
}
<template>
  <p>{{ $tc('wolves', 5) }}</p>
</template>

You can also pass a parameter object with named items as 3rd parameter:

{
    "wolves": "no wolf | one wolf | a pack of {numberOfWolves} wolves"
}
<template>
  <p>{{ $tc('wolves', 5, {numberOfWolves: 5}) }}</p>
</template>

Structuring your translations

You can use nested objects in your translation files to group your translations:

{
    "main-screen": {
        "title": "Main screen",
        "description": "This is the main screen..."
    },
    "about-screen": {
        "title": "About screen",
        "description": "That's what this is all about...."
    },
}

Use a . to access the sub-elements:

<template>
  <p>{{ $t('about-screen.title') }}</p>
</template>

Referencing other translations

You can use links to include other translations in your texts. This is helpful if you want to re-use translations:

{
    "common": {
        "appname": "Vue-Demo",
        "yes": "Yes",
        "no": "No"
    },
    "exit-dialog": {
        "title": "Exit screen",
        "text": "Do you really want to exit @:common.appname ?",
        "button-1": "@:common.yes",
        "button-2": "@:common.no"
    },
}

Switching locales

Switching the locale is quite simple: Just assign the new locale to the locale property of the VueI18n you created in src/i18n.js.

This code gives you a simple combo-box that you can use to switch between languages:

<template>
  <div class="locale-changer">
    <select v-model="$i18n.locale">
      <option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">{{ lang }}</option>
    </select>
  </div>
</template>

<script>
export default {
  name: 'locale-changer',
  data () {
    return { langs: ['en', 'de'] }
  }
}
</script>