Internationalization and localization are great ways 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.
In this tutorial, we’ll show you how to use Vue I18n to implement localization, internationalization, and translation in your Vue.js app.
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 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 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 . .
<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:
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 componentsI personally prefer using .json
files for both small and big applications since it is much easier to maintain.
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.
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!
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.
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" } }
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.
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.
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 and mobile 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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
4 Replies to "Localization in Vue.js with vue-i18n"
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?
Hello Lyra,
I can see the `languageArray` used in `HelloWorld.vue` to create the languge dropdown. We initially hardcode the list and later in the tutorial we used the exported value from `i18n.js`. Maybe you have still kept the old Array?
Meanwhile you can also check the working github repo which might help you fix the issue https://github.com/preetishhs/vue-localization-techniques
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
}
Thank you Preetish. You saved my day!