Advanced localization techniques in Vue.js
Brian Neville-O'Neill
Posted on February 5, 2020
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.
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
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 id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld :msg="$t('hello')" />
</div>
</template>
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.
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:
- 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 - As the number of translations and languages increase, the component starts getting big and ugly.
- 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.
- 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.
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.
Posted on February 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.