VueJS - Internationalization

Since the Internet's boom, more and more web applications are released each day (approximately 576k new websites per day*). Currently, there are over 1.7 billion websites* on the Internet and many of them are accessed by people from all around the world.

Probably, if you create a website, you will want it to be accessed by as many people as possible. To make that happen and also provide a nice experience for your users, you will have to think about how to deliver it in different languages.

* Source: Website Setup

TL;DR

In this article I'm going to show you how to:

  • create a multi-language website using VueJS and Vue i18n;
  • implement a language switcher;
  • use the vuex-persistedstate package to avoid losing state when reloading the page;

Internationalization (i18n)

Let's start with some basic concepts.

If you're still not aware of Internationalization or what i18n really means, here is its official definition:

Internationalization is the design and development of a product, application or document content that enables easy localization for target audiences that vary in culture, region, or language.
(Source: W3.org)

Internationalization is often written i18n (English), where 18 is the number of letters between "i" and "n" in the English word (cool, right?!).

Vue i18n

If you perform a quick search on the internet you will find some solutions to implement i18n to your website or application built using VueJS.
Probably, the most famous (and easy-to-use) one is Vue i18n, an open source plugin for VueJS that provides a very friendly API to implement translation to different target languages in your website.

Installation

Assuming you've already created your VueJS project (take a look at this article if you don't now where to start from), the first step to begin using the plugin is installing it. In a terminal window, go to the root directory of your project and run the following command:

yarn add vue-i18n --save

You might use NPM as well, depending on your project configuration.

Configuration

The Vue i18n package works in a very simple way.
You can set several configurations, but these are the basic ones required for your project to work:

  • the initial language: the language loaded by default;
  • the messages: a simple JSON object that contains the messages (translation keys) used for each one of the languages;

First, create the folder structure that will hold everything together:

  1. Create a folder called i18n in the src directory of your project.

  2. Within the i18n folder, create an index.js file and a folder called messages.

This is how this first index.js file will look like:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import messages from './messages'

Vue.use(VueI18n)

export default new VueI18n({
  locale: 'en',
  messages
})
  1. In this example, we're going to build a part of an application that needs to be delivered in three different languages: English (default), Spanish and Brazilian Portuguese.

Within the messages folder, create three folders named en, es and pt-BR and, inside each one of them (that represents a different language), create two files: one named menu.js and another named index.js .

The files will look like this:

English

// /src/i18n/messages/en/menu.js

export default {
  home: 'Home',
  about: 'About',
  contact: 'Contact'
}
// /src/i18n/messages/en/index.js

import menu from './menu'

export default {
  menu
}

Spanish

// /src/i18n/messages/es/menu.js

export default {
  home: 'Pagina de Inicio',
  about: 'Acerca de',
  contact: 'Contacto'
}
// /src/i18n/messages/es/index.js

import menu from './menu'

export default {
  menu
}

Portuguese (Brazil)

// /src/i18n/messages/pt-BR/menu.js

export default {
  home: 'Início',
  about: 'Sobre',
  contact: 'Contato'
}
// /src/i18n/messages/pt-BR/index.js

import menu from './menu'

export default {
  menu
}

If you need, you may create more levels within the messages object to organise them better. Like this:

export default {
  links: {
    home: {
      label: 'Home',
      help: 'Click here to go to home page'
    },
    about: {
      label: 'About',
      help: 'Click here to know more about us'
    },
    contact: {
      label: 'Contact',
      help: 'Click here to go to reach out to us'
    }

  }
}
  1. Still inside the messages folder, create an index.js file like this:
import en from './en'
import es from './es'
import ptBR from './pt-BR'

export default {
  en,
  es,
  'pt-BR': ptBR
}
  1. In the main.js file, import the i18n package and set it to the Vue instance:
import App from './App.vue'
import i18n from './i18n' 

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

Now your application is ready to take advantage of the vue-i18n plugin. Let's create a simple scenario to use it.

Implementation

We're going to implement a language switcher and put it into a navbar at the top of the page. This switcher will be responsible to set the current locale of the application using Vuex + Vuex Persisted State.

To make things easier, I chose to use Bootstrap Vue. If you don't know it yet, it's worth taking a look. It provides all of the Bootstrap components, wrapped into Vue components :)

Before creating the component itself, we're going to structure a basic Vuex module that will be responsible for managing the language state, we'll also make use of the Vuex Persisted State plugin, to easily store the state in the local storage so that, when refreshing the page, the user does not lose its selected language.

  1. To add Vuex Persist in your project, run the following command in the root directory of your project:
yarn add  vuex-persistedstate --save
  1. Create a file named index.js and a folder named store inside the src directory.

  2. Create folder named modules within store.

  3. Create a file named locale.js inside the modules folder and implement it like this:

// src/store/modules/locale.js

export default {
  namespaced: true,
  state: {
    locale: 'en'
  },
  mutations: {
    setLocale(state, locale) {
      state.locale = locale
    }
  }
}

This is how the store/index.js will look like:

// src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

import locale from './modules/locale'

const persistedState = createPersistedState({
  key: 'vuejs-vue-i18n',
  paths: ['locale']
})

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    locale
  },
  plugins: [persistedState]
})
  1. Now, let's implement the LanguageSwitch.vue component. It will hold all of the available languages and it will use Vuex helpers functions to update the current language:
<!-- src/components/LanguageSwitcher.vue (template) -->
<template>
  <b-nav-item-dropdown :text="currentLocale" right>
    <b-dropdown-item
      :disabled="isCurrentLocale('en')"
      @click="onSetLocale('en')"
    >
      EN
    </b-dropdown-item>
    <b-dropdown-item
      :disabled="isCurrentLocale('es')"
      @click="onSetLocale('es')"
    >
      ES
    </b-dropdown-item>
    <b-dropdown-item
      :disabled="isCurrentLocale('pt-BR')"
      @click="onSetLocale('pt-BR')"
    >
      PT-BR</b-dropdown-item
    >
  </b-nav-item-dropdown>
</template>
// src/components/LanguageSwitcher.vue (script)

<script>
import { mapState, mapMutations } from 'vuex'

export default {
  name: 'LanguageSwitcher',
  computed: {
    ...mapState('locale', ['locale']),
    currentLocale() {
      return this.locale.toUpperCase()
    }
  },
  created() {
    this.$i18n.locale = this.locale
  },
  methods: {
    ...mapMutations('locale', ['setLocale']),
    onSetLocale(locale) {
      this.$i18n.locale = locale
      this.setLocale(locale)
    },
    isCurrentLocale(locale) {
      return this.locale === locale
    }
  }
}
</script>
  1. Now, let's create a simple Navbar.vue component to put the recently created LanguageSwitcher one. Notice that, in this case, we use the global $t helper provided by the Vue i18n plugin to get the proper translation we need to display according to the current locale. It's very simple to use, all you need to do, it call it passing a translation key as argument.

Example:

{{ $t('translation.key') }}

You may also use directly in the script section of your components, if needed:

{
  computed: {
    label() {
       // For this work, you have to create a file named `common.js` inside the folder of each language and export it in its respective `index.js` file.
       return this.$t('common.label')
    }
  },
  methods: {
    getTitle() {
       return this.$t('common.title')
    }
  }
}

This is how our Navbar.vue component will look like:

<!-- src/components/Navbar.vue (template) -->
<template>
  <b-navbar toggleable="lg" type="dark" variant="primary">
    <b-navbar-brand>VueJS vue-i18n</b-navbar-brand>

    <b-navbar-toggle target="nav-collapse" />

    <b-collapse id="nav-collapse" is-nav>
      <b-navbar-nav>
        <b-nav-item :to="{ name: 'Home' }">
          {{ $t('navbar.home') }}
        </b-nav-item>
        <b-nav-item :to="{ name: 'About' }">
          {{ $t('navbar.about') }}
        </b-nav-item>
        <b-nav-item :to="{ name: 'Contact' }">
          {{ $t('navbar.contact') }}
        </b-nav-item>
      </b-navbar-nav>

      <b-navbar-nav class="ml-auto">
        <LanguageSwitcher />
      </b-navbar-nav>
    </b-collapse>
  </b-navbar>
</template>
<!-- src/components/Navbar.vue (script) -->
<script>
import LanguageSwitcher from '@/components/LanguageSwitcher/LanguageSwitcher'

export default {
  name: 'Navbar',
  components: {
    LanguageSwitcher
  }
}
</script>
  1. We're going to create a Layout.vue component that will hold the Navbar and will be used within the Views we are going to create next:
<!-- src/views/Layout.vue (template) -->
<template>
  <b-row>
    <b-col>
      <Navbar />
      <b-container>
        <slot />
      </b-container>
    </b-col>
  </b-row>
</template>
// src/views/Layout.vue (script)

<script>
import Navbar from '@/components/Navbar'

export default {
  name: 'Layout',
  components: {
    Navbar
  }
}
</script>
  1. For everything to work properly, we need to create the views, implement the Layout component into them and add them to the router/index.js file. In this section, the most important thing is to use the global $t helper, provided by the Vue i18n package

src/components/Home.vue

<template>
  <Layout>
    <h1>{{ $t('navbar.home') }}</h1>
  </Layout>
</template>
<script>
import Layout from './Layout'

export default {
  name: 'HomeView',
  components: {
    Layout
  }
}
</script>

src/components/About.vue

<template>
  <Layout>
    <h1>{{ $t('navbar.about') }}</h1>
  </Layout>
</template>
<script>
import Layout from './Layout'

export default {
  name: 'AboutView',
  components: {
    Layout
  }
}
</script>

src/components/Contact.vue

<template>
  <Layout>
    <h1>{{ $t('navbar.contact') }}</h1>
  </Layout>
</template>
<script>
import Layout from './Layout'

export default {
  name: 'ContactView',
  components: {
    Layout
  }
}
</script>

In order to use nested translation keys, the process is simple, since Vue I18n works with the full translation key path, like this:

<template>
  <Layout>
    <h1>{{ $t('navbar.links.contact.label') }}</h1>
  </Layout>
</template>

src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About')
  },
  {
    path: '/contact',
    name: 'Contact',
    component: () => import('@/views/Contact')
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

This is how the application should work after the full implementation:

You can find the fully-working source code in this link!

Other features

Besides translating simple pieces of text, Vue I18n also provides other useful features like:

You may explore the website to find out more about the tool.

I hope you liked it.
Please, comment and share!

Cover image by Ben White

27