Add i18n and manage translations of a Vue.js powered website

Vue + Vue Router + Vue-i18n +

Setting up a Vue.js website with internationalization (i18n) support sounds daunting at first, but it’s actually easier than one might think. For this tutorial we will be using VueI18n. A great package by the core Vue devs.


Checkout the Github repo of vue-i18n-starter to see how to add i18n support to your website.

Initial setup

You need to do two main steps:

  1. Instantiate the plugin by providing a default locale, a fallback and an object containing all the translation strings.
Setup i18n

2. Tell our App about that instance

Pass i18n instance to Vue app

That’s all it takes to have the app i18n ready. Now you can just use {{ $t(‘hello’) }} inside Vue component templates and you will get Hello.

Changing the language

Changing the language is as simple as setting locale in the $i18n property on the Vue instance.

this.$i18n.locale = 'fr'

Now it should display hello in the language you just set.

If you set a language that is not loaded, it will show the fallback language’s translation. If the translation is not available in the fallback it will just return the key, in this case hello. Keys can be deeply nested if needed.

With that said, you could create a basic language switcher, that loops your supported languages and on click it sets the new language in $i18n.locale .

Simple language switcher

Setting the language as a route parameter

Setting it up slightly depends on how you want your URLs to look like. The most popular choice is to have the language param as the first segment of the URL

If you are not using Vue Router to route your app, you would need to figure this next step yourself, but the idea should be the same.

  1. Check if the URL contains the language parameter
  2. Check if the language is supported by the app. If not redirect to a supported one.
  3. Check if the language in the param is the default one. Don’t do nothing if it is.
  4. (Optional) load the language strings
  5. Change the language in the App

Making it work with Vue Router

First you need to tell the Router about the lang parameter. Define it in your Vue Router routes like so:

Setup routes file
Instantiate Router
Pass router instance to App

Essentially we are creating a page whose route is a dynamic property that every time we assume is the language property.

The router can’t distinguish between and as both would fall into the same path matcher. All pages would need to be nested inside the children key of the top level component. About Page would become and it will get rendered inside the router-view.

If you need a page to be matched without the lang in the URL, you have to define it above the lang page definition we just added.

Checking if the lang param is valid and supported by the App can be done inside the beforeEnter guard. It is a function that receives 3 params, the route you want to go to, the one you come from and a callback to call when you are done. Read more about it in the VueRouter Route Guards docs:

Initial Router guard

Test the app. Go to and you should get redirected to

Async loading translation strings

But what about more languages?

Well, we could feed all our language strings in the messages key at once and it would work.

But what if we support 10 languages and each of them has 500 keys? Or 10000 keys? We would be pushing unnecessary amount of code for all our users to load upfront. You lose visitors with extra loading time.

What we could do is only provide the English keys and asynchronously load the rest. If you are using Webpack this is super easy to do with the dynamic import function.

We will do two things.

  • Load the translation strings on demand (async)
  • Feed the strings into Vue-i18n

Lets add this to the beforeEnter guard.

Async load json files

Now we need to extract all of the translation strings into separate json files inside the lang folder. It is a top level folder. This means the English ones as well. To make our code a bit cleaner we will just import the English strings and feed them to the i18n instance.

Separate language files

We are using the path @/lang which is an alias for the top-level folder. If you are using vue-cli its already setup for you. If not, then can do ../lang/${lang}.json.

Using i18n outside Vue components

The way we extracted i18n instance into its own i18n.js file actually gives us the opportunity to use it in many places where the Vue instance isn’t available and we cant use this.$i18n. Such places are:

  • Vuex store
  • Route guards
  • beforeRouteEnter Vue hooks — the Vue instance is not not ready yet, so you can’t use this

Cleaning it up

It all works, but there is some repetition and the route guard is a bit nasty. If you want to change the lang param in the html tag or set the axios Accept-language header, those would need to fit into the beforeEnter guard too.

What about checking for supported languages? Those are defined in a few places. Not really DRY.

What you could do is extract all those repetitions and fit them inside a helper service file that you can then import and use. You could also extract this into Vuex actions, but because it will not be used inside Vue components that often, it doesn’t make sense. That would also make them async.

Check out the example i18n-starter repo to see how you could refactor this to a more clean approach.

Bonus: Managing translation strings using

Managing translation strings is hard work. Having to sync all language strings all the time is a drag.

Fortunately there is a super cool service to the rescue — Its a payed service, that has a free limited plan, but for the amount of time you are saving its well worth it. The free plan has 1 user and 500 keys, which is OK to try it out, even enough for smaller projects.

You can upload the English file as the original source, assign translators, then just download everything into separate .json files when ready. It even detects if someone messed up a {placeholder} in the translations. How cool is that?

You can also use their CLI tool to make this even easier.

Checkout npm run lokalise:down command in package.json for info on how to make the sync-down work.

That’s it!

p.s. if you thoroughly enjoy stuff like this why not check our job openings at Hypefactors?