Add the locale at the root of the URL in VueJS (i18n)

How to add the locale (“en”, “fr”, …) at the root of the URL in VueJS? There are several ways of doing it, so let’s review them.

We will try to achieve something like that :

mysite.com/en/user/john

Please note that we will be using Vue Router & Vue-I18n for internationalization. You can still read this article if you don’t use these Vue plugins. It won’t stop you from getting the big picture and implementing these solutions in your app, but I strongly advise you to use these plugins anyway.

Project setup

Vue-I18n

First, install Vue-I18n. We’ll use npm :

npm install vue-i18n

Then create a translations directory inside src. The newly created folder will have an index.js file containing Vue-I18n’s configuration. We will set the plugin so it lets the browser decide what the default language should be :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// translations/index.js

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

Vue.use(VueI18n);

const messages = {
  en: {
    welcome: 'Welcome !',
  },
  fr: {
    welcome: 'Bienvenue !',
  },
};

const authorisedLanguages = ['en', 'fr'];
const locale = authorisedLanguages.includes(navigator.language) ? navigator.language : 'en';

export default new VueI18n({
  locale,
  fallbackLocale: 'en', // By default, the page is translated into english
  messages,
});

Vue Router

For the moment this is pretty straightforward. Install the router with npm :

npm install vue-router

Create a folder called router and inside of it an index.js file containing the following configuration :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// router/index.js

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

Vue.use(VueRouter);

const routes = [];

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

export default router;

Now, inside main.js, you need to have your app configured that way :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// main.js

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import i18n from './translations';

Vue.config.productionTip = false;

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

Adding the locale in the URL

Go back to the router’s configuration file. We want to tell the router to redirect the users to /:lang (where :lang can be “fr” or “en” in our example) whenever they try to access / (the base route).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// router.index.js

const routes = [
  {
    path: '/',
    redirect: { name: 'lang' },
  },
  {
    path: '/:lang',
    name: 'lang',
    component: Home,
  },
];

For the moment, the Home component will just contain some simple text provided with the translation:

1
2
3
4
5
6
7
<!-- Home.vue -->

<template>
  <div class="home">
  	 <h1>{{ $t('welcome') }}</h1>
  </div>
</template>

The Home component will be displayed but the URL won’t be changed. That’s because the router doesn’t know what to do with the lang parameter. So we’ll make use of the beforeEnter navigation guard to tell it what value to give that parameter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// router/index.js

import i18n from '../translations';

...

{
    path: '/:lang',
    name: 'lang',
    component: Home,
    beforeEnter: (to, from, next) => {
      const locale = to.params.lang; // Retrieve the current locale set in the URL

      // Check if the locale the user is trying to access is authorised.
      // In a larger application that supports lots of languages, you may want to store
      // all the locales in a separate array
      if (!['en', 'fr'].includes(locale)) {
        return next(i18n.locale);
      }

		// Changing the language from the URl (either manually or with a link) is possible this way
      i18n.locale = locale;
      return next();
    },
  },

Now whenever the user wants to access the base of the site, he’ll be redirected to /:lang where :lang is the default language. Also, if you add some translatable text in your User component, the text will be translated even if you switch the locale in the URL to fr. Try it !

Using nested routes

But that’s not enough. We want our app to be more than just a single page that translates “Welcome” into french. There are many ways to structure an app. So let’s see how we would configure our router in a real world application.

In this first case, let’s pretend our app uses nested routes, like that:

/profile                              /posts
+------------------+                  +-----------------+
| Home             |                  | Home            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

The idea is that Home will contain other components, like a menu that will be shared with Profile and Posts. Of course those last two components can also contain their own nested routes.

So, how do we do this? By providing our lang route with children:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// router/index.js

{
    path: '/:lang',
    name: 'lang',
    component: Home,
    children: [
      { path: 'user', name: 'user', component: User }, // This route will be accessible through /en/user or /fr/user
      { path: 'posts', name: 'posts', component: Posts },
    ],
    beforeEnter: (to, from, next) => {
      ...
    },
  },

Do not forget to add a router-view tag in the Home component…

1
2
3
4
5
6
7
8
<!-- Home.vue -->

<template>
  <div class="home">
  	 <h1>{{ $t('welcome') }}</h1>
    <router-view />
  </div>
</template>

…and to add, of course, the two new components:

1
2
3
4
5
6
7
<!-- User.vue -->

<template>
  <div class="user">
    <h2>{{ $t('user') }}</h2>
  </div>
</template>
1
2
3
4
5
6
7
<!-- Posts.vue -->

<template>
  <div class="posts">
    <h2>{{ $t('posts') }}</h2>
  </div>
</template>

Let’s add the translation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// translations/index.js

const messages = {
  en: {
    welcome: 'Welcome !',
    user: 'User profile',
    posts: 'Last posts',
  },
  fr: {
    welcome: 'Bienvenue !',
    user: 'Profil d\'utilisateur',
    posts: 'Derniers messages',
  },
};

The app now works !

So what about sites that do not use nested routes ?

In the very simple app we’ve just created, the user and the posts pages are both children of the Homepage. In lots of cases, we may want to make our pages more distinct, like this for example :

/                                     /posts
+------------------+                  +-----------------+
| Home             |                  | Posts           |
|                  |                  |                 |
|                  |  +------------>  |                 |
|                  |                  |                 |
|                  |                  |                 |
+------------------+                  +-----------------+

                                      /user
                                      +-----------------+
                                      | User            |
                                      |                 |
                      +------------>  |                 |
                                      |                 |
                                      |                 |
                                      +-----------------+

It’s actually quite simple. We’ll have to remove the children array, and set up all the routes as siblings. Also, we’ll use the beforeEach() safeguard, and it will be applied to the router variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const routes = [
  {
    path: '/',
    redirect: 'lang',
  },
  {
    path: '/:lang',
    name: 'lang',
    component: Home,
  },
  {
    path: '/:lang/user',
    name: 'user',
    component: User,
  },
  {
    path: '/:lang/posts',
    name: 'posts',
    component: Posts,
  },
];

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

router.beforeEach((to, from, next) => {
  const locale = to.params.lang; // Retrieve the current locale set in the URL

  // Check if the locale the user is trying to access is authorised.
  // In a larger application that supports lots of languages, you may want to store
  // all the locales in a separate array
  if (!['en', 'fr'].includes(locale)) {
    return next(i18n.locale);
  }

  // Changing the language from the URl (either manually or with a link) is possible this way
  i18n.locale = locale;
  return next();
});

export default router;

You can have access to the source code from the Github repository.

updatedupdated2020-03-292020-03-29