Preetish HS Freelance web developer, digital nomad, and design enthusiast. www.preetish.in

Advanced localization techniques in Vue.js

8 min read 2257

Advanced Localization Techniques In Vue.js

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.

Getting set up

Let’s create a Vue application using the CLI.

vue create localization-app

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

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"
}

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"
}

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>

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

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      {{ $t('welcomeMessage') }}
    </p>
    <div class="lang-dropdown">
      <select v-model="$i18n.locale">
        <option
          v-for="(lang, i) in languageArray"
          :key="`lang${i}`"
          :value="lang"
        >
          {{ lang }}
        </option>
      </select>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    languageArray: ['en', 'fr']
  }
}
</script>

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.

We made a custom demo for .
No really. Click here to check it out.

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
      .
      .

Translations directly in Vue component files

<i18n>
  {
    "en": {
      "welcome": "Welcome!"
    },
    "fr": {
      "welcome": "Bienvenue"
    }
  }
</i18n>

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()
})

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()
})

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()
})

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()]
})

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>

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'
    }
  }
}

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"
  }
}

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"
  }
}

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

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(() => {})
    }
  }
}

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. https://logrocket.com/signup/

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 - .

Preetish HS Freelance web developer, digital nomad, and design enthusiast. www.preetish.in

3 Replies to “Advanced localization techniques in Vue.js”

  1. Hi Preetish! I’m following along with your tutorial and after making the changes to App.vue and HelloWorld.vue and attempting to run the app, I’m getting the error ‘languageArray:’ is defined but never used. Do you know why this might be?

  2. Hi,
    Thank you very much for such detailed article and knowledge share.

    I’ve made a small change to cehckDefaultLanguage because forEach continues to iterate despite already having a match.

    function checkDefaultLanguage () {
    let matched = null
    const supportedLanguages = Object.getOwnPropertyNames(loadLocaleMessages())
    matched = supportedLanguages.find(lang => lang === navigator.language)
    if (!matched) {
    const navigatorLanguagePartials = navigator.language.split(‘-‘)[0]
    matched = supportedLanguages.find(lang => lang === navigatorLanguagePartials)
    }
    if (!matched) {
    const navigatorLanguagePartials = navigator.language.split(‘-‘)[0]
    matched = supportedLanguages.find(lang => lang.split(‘-‘)[0] === navigatorLanguagePartials)
    }
    return matched
    }

Leave a Reply