Advanced localization techniques in Vue.js

bnevilleoneill

Brian Neville-O'Neill

Posted on February 5, 2020

Advanced localization techniques in Vue.js

Written by Preetish HS✏️

Localization is a great way to make your web application more accessible to a wider audience and provide a better user experience. For businesses in particular, localization helps strengthen global presence, thus creating potential for greater revenue. Let’s look at some techniques to implement localization in Vue.js.

LogRocket Free Trial Banner

Getting set up

Let’s create a Vue application using the CLI.

vue create localization-app
Enter fullscreen mode Exit fullscreen mode

Select vue-router and vuex, as we will need them later.

After creating the project, let’s add our translation library, vue-i18n. We also have a Vue CLI package for that, so we can simply run the following:

cd localization-app
vue add i18n
Enter fullscreen mode Exit fullscreen mode

Hello, World Example

Since we installed the vue-i18n package, it automatically does all required setup. It also creates a locale folder, with en.json as our default language file.

//en.json
{
  "hello": "hello i18n !!",
  "welcomeMessage": "Welcome to Advanced Localization techniques tutorial"
}
Enter fullscreen mode Exit fullscreen mode

Let’s create one more file in the directory for French translations, fr.json, and add the following code:

//fr.json
{
  "hello": "Bonjour i18n !!",
  "welcomeMessage": "Bienvenue dans le didacticiel sur les techniques de localisation avancées"
}
Enter fullscreen mode Exit fullscreen mode

To use it in our component, open App.vue. There is some default code present, with msg being passed to the <hello-world> component. Let’s edit it as follows:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld :msg="$t('hello')" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In the HelloWorld.vue file, let’s remove some code and have minimal code for learning:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld :msg="$t('hello')" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Finally, move the i18n.js file in the root directory to the plugins directory for better structure. When you run the app, you’ll see Hello i18n in English. Since we haven’t set any preference, it takes the fallback language.

Vue I18n Install And Setup

Directory structure

We can have separate json files for different languages in the locales folder.

src
|--plugins
|  |--i18n.js
|--locales
|  |--formats
|  |--en.json
|  |--fr.json
|  |--zh.json
|  |--de.json
      .
      .
Enter fullscreen mode Exit fullscreen mode

Translations directly in Vue component files

<i18n>
  {
    "en": {
      "welcome": "Welcome!"
    },
    "fr": {
      "welcome": "Bienvenue"
    }
  }
</i18n>
Enter fullscreen mode Exit fullscreen mode

We can have our component-specific translations in their own components. While this might seem like nice isolation from other locales, there are more cons than pros. It would work for small apps with fewer translations, but as the app starts getting big, we’ll soon run into problems, like:

  1. You’ll wind up duplicating efforts. For example, the text Welcome might be used in multiple places (login screen, store page, etc.), and you’d have to write the same translations for each of these components
  2. As the number of translations and languages increase, the component starts getting big and ugly.
  3. Generally, developers don’t manage translations; there may be a language translation team with minimal coding experience. It becomes almost impossible for them to figure out the components and syntax to update translations.
  4. You’re not able to share locales among different components.

I personally prefer using .json files for both small and big applications since it is much easier to maintain.

Using the browser’s default language

We are using English as our default language now. If someone with their browser language set to French also sees the website in English, they have to manually change the language using the dropdown. For a better user experience, the application should automatically change its language based on the browser’s default language. Let’s see how this is done.

In the i18n.js file, let’s assign navigator.language (the browser’s default language) to locale. Browsers generally prefix the default language like en-US or en-GB. We just need the first part for our setup, hence we use navigator.language.split('-')[0]:

// plugins/i18n.js
export default new VueI18n({
  locale:
    navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE || 'en',
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
  messages: loadLocaleMessages()
})
Enter fullscreen mode Exit fullscreen mode

But let’s say we do have region-specific modifications in the same language. We generally follow the naming convention where we suffix the region after the language (e.g., en-US.json , en-GB.json). To get the correct language for the region, we need to do a few more operations than before:

function checkDefaultLanguage() {
  let matched = null
  let languages = Object.getOwnPropertyNames(loadLocaleMessages())
  languages.forEach(lang => {
    if (lang === navigator.language) {
      matched = lang
    }
  })
  if (!matched) {
    languages.forEach(lang => {
      let languagePartials = navigator.language.split('-')[0]
      if (lang === languagePartials) {
        matched = lang
      }
    })
  }
  return matched
}
export default new VueI18n({
  locale: checkDefaultLanguage() || process.env.VUE_APP_I18N_LOCALE || 'en',
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
  messages: loadLocaleMessages()
})
Enter fullscreen mode Exit fullscreen mode

The loadLocaleMessages() method is already available by default; we make use of the same method to extract the filenames of our json files. Here, we get ['en-GB', en-US', 'fr']. Then we write a method called checkDefaultlanguage(), where we first try to match the full name. If that’s unavailable, then we match just the first two letters. Great, this works!

Let’s consider another scenario. Say our default language is fr, and the browser language is en-IN. en-IN is not present in our language list, but showing French (the default language) doesn’t make much sense because we do have English from other regions. Though it’s not quite the same, it’s still better than showing a totally different language. We need to modify our code one more time to work for this scenario.

function checkDefaultLanguage() {
  let matched = null
  let languages = Object.getOwnPropertyNames(loadLocaleMessages())
  languages.forEach(lang => {
    if (lang === navigator.language) {
      matched = lang
    }
  })
  if (!matched) {
    languages.forEach(lang => {
      let languagePartials = navigator.language.split('-')[0]
      if (lang === languagePartials) {
        matched = lang
      }
    })
  }
  if (!matched) {
    languages.forEach(lang => {
      let languagePartials = navigator.language.split('-')[0]
      if (lang.split('-')[0] === languagePartials) {
        matched = lang
      }
    })
  }
  return matched
}
export const selectedLocale =
  checkDefaultLanguage() || process.env.VUE_APP_I18N_LOCALE || 'en'
export const languages = Object.getOwnPropertyNames(loadLocaleMessages())
export default new VueI18n({
  locale: selectedLocale,
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
  messages: loadLocaleMessages()
})
Enter fullscreen mode Exit fullscreen mode

Here we split both strings (i.e., the browser default and the JSON filenames) and finally match en-IN with en-GB, which is way better than showing French. I am also exporting a few constants, which we’ll be using later.

Persisting language preference

Let’s manually change the language to French now using the dropdown we created. The texts get translated to French. Now refresh the page or close the tab and reopen it. The language is reset to English again!

This doesn’t make for good user experience. We need to store the user’s preference and use it every time the application is used. We could use localStorage, save and fetch every time, or we can use Vuex and the vuex-persistedstate plugin to do it for us.

Let’s do it the Vuex way. First we need to install the plugin:

npm install --save vuex-persistedstate


//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import i18n, { selectedLocale } from '@/plugins/i18n'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    locale: selectedLocale
  },
  mutations: {
    updateLocale(state, newLocale) {
      state.locale = newLocale
    }
  },
  actions: {
    changeLocale({ commit }, newLocale) {
      i18n.locale = newLocale // important!
      commit('updateLocale', newLocale)
    }
  },
  plugins: [createPersistedState()]
})
Enter fullscreen mode Exit fullscreen mode

Instead of using component state, let’s use Vuex to store and mutate the change in language. The vuex-persistedstate plugin will store the locale variable in localStorage. When it is set, every time the page reloads, it fetches this data from localStorage.

Now we need to link this data to our language selection dropdown.

<template>
  <div class="lang-dropdown">
    <select v-model="lang">
      <option
        v-for="(lang, i) in languageArray"
        :key="`lang${i}`"
        :value="lang"
      >
        {{ lang }}
      </option>
    </select>
  </div>
</template>
<script>
import { languages } from '@/plugins/i18n'
export default {
  data() {
    return {
      languageArray: languages
    }
  },
  computed: {
    lang: {
      get: function() {
        return this.$store.state.locale
      },
      set: function(newVal) {
        this.$store.dispatch('changeLocale', newVal)
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Instead of hardcoding the language list, we are now importing it from the i18n.js file (we had exported this list before). Change the language and reload the page — we can see that the site loads with the preferred language. Great!

Date/time localization

Different countries and regions have different time formats, and the names of days and months are, of course, written in their native languages. To localize the date and time, we need to pass another parameter, dateTimeFormats, while initializing vue-i18n.

Internally, the library uses ECMA-402 Intl.DateTimeFormat, hence we need to write our format in the same standards to work. Create a file dateTimeFormats.js inside src/locales/formats:

//locales/formats/dateTimeFormats.js
export const dateTimeFormats = {
  fr: {
    short: {
      day: 'numeric',
      month: 'short',
      year: 'numeric'
    },
    long: {
      weekday: 'short',
      day: 'numeric',
      month: 'short',
      year: 'numeric',
      hour: 'numeric',
      minute: 'numeric',
      hour12: true
    }
  },
  'en-US': {
    short: {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    },
    long: {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
      weekday: 'short',
      hour: 'numeric',
      minute: 'numeric'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As shown above, we just need to mention the items such as day, month, etc., and the library does all the formatting and translation for us based on the locale selected.

Reusing translations

As the app starts growing, our localization file contents also start growing. For better readability, we need to nest the translations in our JSON file based on the categories or components, depending on the application. Soon we’ll see a lot of repeated messages, or common words such as username, hello, or click here appearing in many components.

//en.json
{
 "homepage": {
    "hello": "hello i18n !!",
    "welcomeMessage": "Welcome to Advanced Localization techniques tutorial",
    "userName": "Username",
    "login": "Login"
  },
  "login": {
    "userName": "Enter Username",
    "password": "Enter Password",
    "login": "Login"
  },
  "forgotPassword": {
    "email": "Email",
    "continue": "Click to get recovery email",
    "submit": "Click to get Login"
  }
}
Enter fullscreen mode Exit fullscreen mode

We can see that translations like userName and login have already started repeating. If we need to update one text, we have to update it at all places so that it reflects everywhere. In medium to large apps, we’ll have thousands of lines of translations in each JSON file. If we use translations from different nested objects in one component, it starts becoming hard to track and debug.

We should group them based on Category instead. Even then, we’ll still encounter some duplicates. We can reuse some translations by using links, like below:

//en.json
{
 "homepage": {
    "hello": "hello i18n !!",
    "welcomeMessage": "Welcome to Advanced Localization techniques tutorial",
    "userName": "Username",
    "login": "Login"
  },
  "login": {
    "userName": "Enter @:homepage.userName",
    "password": "Enter Password",
    "login": "@:homepage.login"
  },
  "forgotPassword": {
    "email": "Email",
    "continue": "Click to get recovery @:forgotPassword.email",
    "submit": "Click to get @:login.login"
  }
}
Enter fullscreen mode Exit fullscreen mode

Using translations with vue-router

Right now, we can’t know in which language the website is being displayed just by seeing the URL localhost:8080. We need it to show something like localhost:8080/fr, i.e., when the user opens the root URL localhost:8080, we need to redirect them to localhost:8080/fr.

Also, when the user changes the language to English using the dropdown, we need to update the URL to localhost:8080/en. There are multiple ways to do this, but since we are already using Vuex to maintain our locale state, let’s use that to implement this feature.

Let’s create one more page called About.vue and add some content there. The /router/index.js file should look like this:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import App from '@/App.vue'
import { languages } from '@/plugins/i18n'
import store from '@/store'
import About from '@/views/About.vue'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'root',
    beforeEnter(to, from, next) {
      next(store.state.locale)
    }
  },
  {
    path: '/:lang',
    component: App,
    beforeEnter(to, from, next) {
      let lang = to.params.lang
      if (languages.includes(lang)) {
        if (store.state.locale !== lang) {
          store.dispatch('changeLocale', lang)
        }
        return next()
      }
      return next({ path: store.state.locale })
    },
    children: [
      {
        path: '',
        name: 'home',
        component: Home
      },
      {
        path: 'about',
        name: 'about',
        component: About
      }
    ]
  }
]
const router = new VueRouter({
  mode: 'history',
  routes
})

export default router
Enter fullscreen mode Exit fullscreen mode

We are first redirecting the request we get for root URL (/) to /:lang by passing the current locale next(store.state.locale).

Case 1 : Changing the URL manually to localhost:8080/en-US. Since our website supports en-US, this will call our store action to also change the language to English.

Case 2 : We change the language using the dropdown. This should also update the URL. To do this, we need to watch the changes to our locale state in App.vue.

export default {
  name: 'app',
  computed: mapState(['locale']),
  watch: {
    locale() {
      this.$router.replace({ params: { lang: this.locale } }).catch(() => {})
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You can find the GitHub repo for this project here.

There we have it!

We learned some of the advanced ways to design localizations in an application. The vue-i18n documentation is also well maintained and a great resource to learn the features and concepts used for localization in Vue. Combining both techniques, we can build solid and efficient localization in our application so that it can cater a wider audience.


Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - Start monitoring for free.


The post Advanced localization techniques in Vue.js appeared first on LogRocket Blog.

💖 💪 🙅 🚩
bnevilleoneill
Brian Neville-O'Neill

Posted on February 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related