Software localization

The Answers to All Your Questions About Gridsome Localization

From number, named, as well as date and time formating, this guide brings everything you need to know about Gridsome localization.
Software localization blog category featured image | Phrase

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.

English translation welcome key | Phrase

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:

  1. If the language passed is the same as the currently selected language, it returns a promise that resolves to the same language.
  2. If the language passed is not the same but the translations are already present, call seti18nLanguage() with the selected language.
  3. 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"

}

We have to use the $tc() (Translation Count) instead of plain $t():

<p>{{ $tc('style', 1) }}</p>

<p>{{ $tc('style', 2) }}</p>
// output

Style

Styles

Our countable translations look much better now!

If you need to perform even more complex counting, the library can help you with it too (see it in action in the example below). We can also have custom pluralization rules for languages that have different grammatical number systems.

{

  "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

Different languages and regions represent numbers (currency amounts, date, time, etc.) in different ways. Some use comma-separated numbers, some space-separated numbers. Currency symbols can also be prefixed or suffixed. Because of all this, displaying numbers properly is essential if you want to have an impeccable user experience, acquire, and retain users.

To implement number formatting, let us create a new directory called formats and add a numberFormats.js file. Here, we can define all our number formats.

module.exports = {

  en: {

    currency: {

      style: 'currency',

      currency: 'USD',

    },

  },

  'en-IN': {

    currency: {

      style: 'currency',

      currency: 'INR',

    },

  },

  fr: {

    currency: {

      style: 'currency',

      currency: 'EUR',

    },

  },

}

Based on Intl.NumberFormat, the library will automatically fetch the currency format and symbol for us. Now, we should let our library know that we need to use these formats. Let us add this file to our i18n configuration file:

const numberFormats = require('../src/formats/numberFormats.js')

module.exports = {

  use: 'gridsome-plugin-i18n',

  options: {

    locales: ['en', 'fr'],

    fallbackLocale: 'en',

    defaultLocale: 'en',

    numberFormats,

  },

}

We can now use these in our components. Here, we use the $n() format, which will let the library know that we are trying to access the number formats. We can explicitly pass the locale to the method as shown below. If we don’t pass, it will take the currently selected language. The number format for that language needs to be defined in the numberFormats.js file so that it works.

<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

Similar to number localization, date and time formats vary based on language and region. We follow the same procedure and create another file called dateTimeFormats.js to define our date and time formats for different languages.

Multiple date formats are used in different parts of the application. We can give specific names for these use cases, such as shortlongreportDateFormat, etc. For each of these, we can define our own formats.

For example:

  1. Last login time format: Sat, 20 Jun, 2020, 9:55 pm
  2. 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, I sincerely hope this Gridsome localization tutorial could help you kick it off right. To learn more techniques for localizing Gridsome apps, visit the official vue-i18n docs and gridsome-plugin-i18n docs. If you already feel confident and want to start internationalizing yourself, make sure you try Phrase for free. With its strong API and powerful toolkit, it will automate your i18n and l10n process from the very start!

Interested in learning more about software internationalization and translation with Vue and other frameworks? Have a look at these tutorials below: