Software localization
The Answers to All Your Questions About Gridsome Localization
Since Gridsome is powered by Vue.js, gridsome-plugin-i18n
is written on top of the most popular Vue.js i18n plugin vue-i18n
, with additional localization features for Gridsome applications.
If you are already familiar with vue-i18n
, you can use almost all its translation features. To find out more about the core vue-i18n
library, check out our Vue internationalization guide.
You can find the entire code for the demo app used in this tutorial in our GitHub repo.
Library Installation and Setup
Let's set up a Gridsome application using the CLI; to install a CLI tool:
yarn global add @gridsome/cli OR npm install --global @gridsome/cli
Creating the project using the CLI:
gridsome create gridsome-i18n-demo
After the setup is complete, we can run the application:
cd gridsome-i18n-demo gridsome develop
Open http://localhost:8080/ in your browser, and you'll see the Gridsome app running.
Now, let's install the gridsome-plugin-i18n
library.
yarn add gridsome-plugin-i18n OR npm install gridsome-plugin-i18n --save
Gridsome has a config file, gridsome.config.js
, where we initialize our plugin. Beforehand, let's create a directory called plugins
, as well as a file inside that directory, i18n.js.
Add the following content into the file:
module.exports = { use: 'gridsome-plugin-i18n', options: { locales: ['en', 'fr'], defaultLocale: 'en', messages: { en: require('../src/locales/en.json'), fr: require('../src/locales/fr.json'), }, }, }
In our demo application, we will use English and French. In our configuration file above, the default language is set to English. We have also specified the path for our translation files. Now we have to add this configuration file to gridsome.config.js
:
const i18n = require('./plugins/i18n') module.exports = { siteName: 'Gridsome-i18n-demo', plugins: [i18n], }
Setting Up External Translation Files
We define key-value pairs for each language, where the key remains the same for all languages, and the value is the translation in that language. We create separate JSON
files for each language, so that it's maintainable when the application scales. Let's create a locales
folder in the src
directory and add the en.json
and fr.json
translation files:
{ "welcome": "Welcome to Gridsome i18n!" }
{ "welcome": "Bienvenue sur Gridsome i18n!" }
Translations Inside .vue Files
The Gridsome i18n library is built on top of vue-i18n
, and the latter allows us to declare our translations directly in Vue SFC (Single File Components). This option is suitable only for those translations that are specific for those components and will not be used in any other files. It will be hard to maintain translations inside components as the application grows.
<i18n> { "en": { "hello": "hello!" }, "fr": { "hello": "Bonjour!" } } </i18n> <template> <div class="main"> <p> {{ $t('hello') }} </p> </div> </template>
Using external translation files is always the recommended approach. Accordingly, to move forward, we will be using only external files in this article. Let's quickly test if our setup works by modifying Index.vue
in that we add a translation for welcome
we just created:
<template> <Layout> <h1>{{ $t('welcome') }}</h1> </Layout> </template> <script> export default {} </script>
Here, $t()
tells our Gridsome app to use the translation version of the key
based on the selected language. Now open http://localhost:8080/ in your browser again, and you will see the English translation associated with the welcome
key.
Now open plugins/i18n.js
and change the defaultLocale
option to fr
, reload the application, and you will see the French translation.
There is one tiny little problem, though. Try to modify the translation text to something else and save the file. We will not see any changes in the UI because hot reloading does not work in our setup yet. To configure hot reloading, let us go back to plugins/i18n.js
and remove the messages
object (the object which contains the reference to the locale files):
module.exports = { use: 'gridsome-plugin-i18n', options: { locales: ['en', 'fr'], pathAliases: { en: 'en', fr: 'fr', }, defaultLocale: 'en', }, }
We have to load the translation files in main.js
as shown below for the hot reloading to work.
import DefaultLayout from '~/layouts/Default.vue' export default function(Vue, { router, head, isClient, appOptions }) { appOptions.i18n.setLocaleMessage('en', require('./locales/en.json')) appOptions.i18n.setLocaleMessage('fr', require('./locales/fr.json')) Vue.component('Layout', DefaultLayout) }
Setting the Active Locale Manually
Similar to most of the applications, we need to build a language switcher dropdown to switch between languages.
Let's create a new component LocaleSwitcher.vue
<template> <select v-model="currentLocale" @change="localeChanged"> <option v-for="locale in availableLocales" :key="locale" :value="locale">{{ locale }}</option> </select> </template> <script> export default { name: "LocaleSwitcher", data: function () { return { currentLocale: this.$i18n.locale.toString(), availableLocales: this.$i18n.availableLocales } }, methods: { localeChanged () { this.$router.push({ path: this.$tp(this.$route.path, this.currentLocale, true) }) } } } </script>
In the code above, we can see that any time we have to get the current locale, we can use $i18n.locale
, just like we do in Vanilla JS. Since the library also localizes the routes by appending the translation automatically (e.g., https://localhost:8080
is redirected to https://localhost:8080/en/
), we also need to update the route path as shown in the code. The $tp()
method provided by gridsome-plugin-i18n
will allow us to localize the current path. We will get into the details of path localization later on.
Let's import this component into our main Index.vue
file:
<template> <Layout> <g-image alt="Example image" src="~/favicon.png" width="135" /> <locale-switcher></locale-switcher> <h1>{{ $t('welcome') }}</h1> </Layout> </template> <script> import LocaleSwitcher from '@/components/LocaleSwitcher.vue' export default { components: { LocaleSwitcher, }, } </script>
Now we can easily switch between English and French simply by using the dropdown.
Asynchronous Translation File Loading
Currently, we are loading all the locales in our main.js
file. The more our application scales, the more languages, i.e. translations we will have to manage. Adding all our translation files to our bundle will simply increase the initial page load time and, hence, decrease the initial performance.
To lazy load translations, we can make use of webpack's Lazy Loading feature. Whenever a user changes the language via the locale switcher we just built, we will try to fetch the language file asynchronously. Let us create a loadLanguageAync()
method and add that functionality. Since this method might be required in other components later on, we need to create a mixin
directory for sharing that code.
After creating the mixins
directory and a languageMixin.js
file, add the following content:
export const languageMixin = { data() { return { localeList: ['en', 'fr'], } }, methods: { setI18nLanguage(lang) { this.$i18n.locale = lang this.$router.push({ path: this.$tp(this.$route.path, lang, true), }) return lang }, loadLanguageAsync(lang) { // If the same language if (this.$i18n.locale === lang) { return Promise.resolve(lang) } // If the language was already loaded if (this.$i18n.availableLocales.includes(lang)) { return Promise.resolve(this.setI18nLanguage(lang)) } // If the language hasn't been loaded yet return import(/* webpackChunkName: "lang-[request]" */ `../locales/${lang}.json`).then( messages => { this.$i18n.setLocaleMessage(lang, messages.default) this.$i18n.availableLocales.push(lang) return Promise.resolve(this.setI18nLanguage(lang)) } ) }, }, }
This might be a bit complicated at first sight, so let us go through it step by step. We have a seti18nLanguage()
method that will take lang
as a parameter and simply set that language.
loadLanguageAsync()
does three things:
- If the language passed is the same as the currently selected language, it returns a promise that resolves to the same language.
- If the language passed is not the same but the translations are already present, call
seti18nLanguage()
with the selected language. - If the language passed is not loaded, load that language file asynchronously.
Finally, we have to modify LocaleSwitcher.vue
to use this method:
<template> <select :value="currentLocale" @change="localeChanged"> <option v-for="locale in localeList" :key="locale" :value="locale"> {{ locale }} </option> </select> </template> <script> import { languageMixin } from '@/mixins/languageMixin' export default { name: 'LocaleSwitcher', mixins: [languageMixin], data: function() { return { currentLocale: this.$i18n.locale, } }, methods: { localeChanged(event) { this.loadLanguageAsync(event.target.value).catch(() => { console.log('Async language fetch failed') }) }, }, } </script>
Locale Auto-Detection
To improvise the user experience we can also read the browser language and set the locale of the website automatically if we support that locale. We can use navigator.language
to get the browser language in the string format. Typically, the language (en-US
) will have two parts, where the first part is the language (en
), and the second part is the region (US
). In this article, we have only one English translation, so we need to autoselect English for browser languages, such as en-US
, en-IN
, en-GB
, etc.
We can get access to navigator
only after the Vue instance has been created in client-side. To prevent build errors, it is crucial to check for process.isClient
before we run this part of the code.
Gridsome does not provide App.vue
file by default. We have to check for browser language in the starting point of the application only once, so let us create an App.vue
file in our root directory.
<template> <Layout> <router-view /> </Layout> </template> <script> import { languageMixin } from '@/mixins/languageMixin' export default { mixins: [languageMixin], created() { if (process.isClient) { this.fetchBrowserLocale() } }, methods: { fetchBrowserLocale() { let fetchedLocale = navigator.language.split('-')[0] this.loadLanguageAsync(fetchedLocale).catch(() => { console.log('Async language fetch failed') }) }, }, } </script>
In the code above, fetchBrowserLocale()
will fetch the browser language and only read the first part of the string. We will make use of the same loadLanguageAsync()
method by just importing our mixin
created previously.
Fallback for Missing Translations in the Active Locale
When we do not have translations for particular keys in a language, we can define a fallback locale that the plugin can use. This can also be used when we are partially translating the application. To declare a fallback language, we just have to add the fallbackLocale
option while initializing the library.
module.exports = { use: 'gridsome-plugin-i18n', options: { locales: ['en', 'fr'], pathAliases: { en: 'en', fr: 'fr', }, fallbackLocale: 'en', defaultLocale: 'en', }, }
Named Formatting
There are instances where you cannot translate every word, for example, proper nouns (first or family names, places), URLs, etc. Sometimes, we also get data from a web service call like username and need to append it between other pieces of text (e.g., "Hello, John, welcome!").
One way is to define separate translations for "hello" and "welcome", and then do something like the following:
<p> {{ $t('hello') }} {{ data.username }} {{ $t('welcome') }} </p>
This might work for English, but in some languages, the placement of a username might be different. Thus, we can make use of the named formatting technique as follows:
{ "welcomeMessage": "Hello, { username } Welcome!" }
<p>{{ $t('welcomeMessage', { username : data.username }) }}</p>
Here, the username
variable in the translation will get substituted from the username
passed through the component.
If we have multiple text pieces that do not need translation, we can also use the list method.
{ "welcomeGreeting": "Welcome! {0}, Your Email: {1} has been verified successfully" }
<p>{{ $t('copyrightMessage', ['Preetish HS', 'contact@preetish.in']) }}</p>
Plurals
It is often difficult to predict quantity without actual data. For example, you would see text pieces, such as style(s)
or category(s)
, used when you cannot predict the number of items. This would work more or less, but we can take advantage of the library’s features and apply proper pluralization. To achieve this, we can pass the quantity value, and the package will send the appropriate translation.
{ "style": "Style | Styles" }
:
<p>{{ $tc('style', 1) }}</p> <p>{{ $tc('style', 2) }}</p>
// output Style Styles
{ "apple": "no apples | one apple | {count} apples", "banana": "no bananas | {n} banana | {n} bananas" }
<p>{{ $tc('apple', 10, { count: 10 }) }}</p> <p>{{ $tc('apple', 10) }}</p> <p>{{ $tc('apple') }}</p> <p>{{ $tc('apple', 0) }}</p> <p>{{ $tc('banana') }}</p> <p>{{ $tc('banana', 0) }}</p> <p>{{ $tc('banana', 1, { n: 1 }) }}</p> <p>{{ $tc('banana', 1) }}</p> <p>{{ $tc('banana', 100, { n: 'too many' }) }}</p>
//output 10 apples 10 apples one apple no apples 1 banana no bananas 1 banana 1 banana too many bananas
🗒️ Note » When we don't pass the second argument to $tc()
, it would pass one internally as a second argument, hence, taking the second translation. It is a best practice to explicitly pass the second argument to avoid confusion.
Number Formatting
module.exports = { en: { currency: { style: 'currency', currency: 'USD', }, }, 'en-IN': { currency: { style: 'currency', currency: 'INR', }, }, fr: { currency: { style: 'currency', currency: 'EUR', }, }, }
const numberFormats = require('../src/formats/numberFormats.js') module.exports = { use: 'gridsome-plugin-i18n', options: { locales: ['en', 'fr'], fallbackLocale: 'en', defaultLocale: 'en', numberFormats, }, }
<p>{{ $n(1000000, 'currency') }}</p> <p>{{ $n(7000000, 'currency', 'en-IN') }}</p> <p>{{ $n(7000000, 'currency', 'fr') }}</p>
//output $1,000,000.00 ₹70,00,000.00 7 000 000,00 €
Currency looks much better with a symbol and proper formatting, for example, the international number system for Dollars (USD) or the Indian number system for Rupees (INR).
Date and Time Formatting
Multiple date formats are used in different parts of the application. We can give specific names for these use cases, such as short
, long
, reportDateFormat
, etc. For each of these, we can define our own formats.
For example:
- Last login time format:
Sat, 20 Jun, 2020, 9:55 pm
- Transaction date format:
Jun 27, 2020
The table below shows the properties we can have in a date-time input; the following table shows the values each property can take.
Reference: ECMA-International
module.exports = { en: { short1: { year: 'numeric', month: 'short', day: 'numeric', }, long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric', }, }, 'en-IN': { short1: { year: 'numeric', month: 'short', day: 'numeric', }, long: { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true, }, }, }
Just like number formats, we have to add dateTimeFormats
to our i18n configuration as well:
const numberFormats = require('../src/formats/numberFormats.js') const dateTimeFormats = require('../src/formats/dateTimeFormats.js') module.exports = { use: 'gridsome-plugin-i18n', options: { locales: ['en', 'fr'], fallbackLocale: 'en', defaultLocale: 'en', numberFormats, dateTimeFormats, }, }
To use these formats in our application, we use the $d()
notation. Similar to number formats, if we don’t explicitly pass the locale
, the currently selected locale will be used. If the latter does not have a format defined, then the fallback language format will be used. Hence, you need to make sure you have formats defined for the supported locales.
<p>{{ $d(new Date(), 'short') }}</p> <p>{{ $d(new Date(), 'long', 'en-IN') }}</p>
//output Jun 12, 2020 Mon, 12 Jun, 2020, 11:45 am
Just like our number formats, the library will automatically format the date and time based on International standards.
Route Path Localization
By default, the plugin will automatically append the language code to the route path (e.g., http:// localhost:8080/about
will redirect to http://localhost/8080/<selected-locale>/about
). Gridsome will internally create separate routes for each language as shown below:
export default [ { path: "/fr/about/", component: c1, meta: { locale: "fr" } }, { path: "/en/about/", component: c1, meta: { locale: "en" } }, // more routes... ]
In case you didn't notice, there is no route for just /about/
. Because of this change, we cannot simply hardcode the path on the router link like below. It will simply not be able to match the exact route and would result in 404.
<g-link class="nav__link" to="/about">About</g-link> // Will not work!
The plugin does provide us with a helper function called $tp()
that can dynamically get the path based on the language selected.
<g-link class="nav__link" :to="$tp('/about')">About</g-link> // works!
Let's say we have locales like en-GB
, fr-fr
, etc. Now, by default, Gridsome will create a path for localhost:8080/en-GB/about
. If we need our own aliases for the path, we can do so by mentioning the pathAliases
in our configuration file.
const numberFormats = require('../src/formats/numberFormats.js') const dateTimeFormats = require('../src/formats/dateTimeFormats.js') module.exports = { use: 'gridsome-plugin-i18n', options: { locales: ['en', 'fr'], pathAliases: { en: 'en', fr: 'fr', }, fallbackLocale: 'en', defaultLocale: 'en', numberFormats, dateTimeFormats, }, }
The library will now use pathAliases
to create the route paths, so the About page will be localhost:8080/en/about
instead of using our default locale string.
Conclusion
Internationalizing your Gridsome will make it accessible to a wider audience across the globe. With this tutorial,
Interested in learning more about software internationalization and translation with Vue and other frameworks? Have a look at these tutorials below: