The Only Nuxt.js Tutorial on I18n You’ll Ever Need

Search engine optimization (SEO) is a great way to reach more people organically and conquer new markets. Nuxt, a popular JavaScript framework, can significantly help your application rank better on Google. This tutorial will walk you through the internationalization process with Nuxt.js from start to finish.

Search engine optimization (SEO) is a core element of a strong global growth strategy. To be able to run your application on a global scale, you need to tear down this barrier right from the start.

Nuxt is a popular JavaScript framework that can significantly help your application with SEO to get better rankings on Google. Based on Vue.js, it supports both the Single Page Application (SPA) and Server Side Rendering (SSR) format.

Since it supports pre-rendering and Vue-meta out of the box, it has been a go-to framework for all the applications that need better SEO but still use the quite awesome Vue.js syntax and libraries.

In this end-to-end Nuxt.js tutorial, we’ll show you how to internationalize your Nuxt.js app and make it fit for the global marketplace. Off we go!

Getting Started With Our Nuxt.js Tutorial

For this tutorial, we’ll need the following libraries:

  1. create-nuxt-app – to scaffold our Nuxt application
  2. nuxt-i18n – to localize our application
Nuxt provides an easy way to scaffold the basic structure with various options by the create-nuxt-app utility. It is not mandatory to install this utility first, we could rather use npx and use it directly.

Let’s create a Nuxt.js app using the following command:

npx create-nuxt-app nuxt-intl

You can add any entries of your choice for Project name, Project description, and Author name. We can choose between npm and yarn. Go simply with the one you feel most comfortable with. It is a good practice to always use ESLint to detect syntax errors. I prefer using Prettier as it makes my code look properly formatted, and when multiple developers work on the same code, it forces them to use a particular format so that the code remains uniform.

My setup is as follows:After the installation is complete, navigate to the project directory and run the application:

cd nuxt-intl 
npm run dev

Open http://localhost:3000/ in your browser and you’ll see the default application page.

Localization Basics

In this tutorial, we’ll use a library called nuxt-i18n, based on vue-i18n, with some Nuxt-specific localization options. Before installing the nuxt-i18n package, let us go through the basics of localization and have a look at the variety of options available in this library.

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.

// English
{
  "hello": "Hello"
}

// French
{
  "hello": "Bonjour"
}

We have two translation options.

  1. Inside Vue Components
  2. External JSON or Js files.

Translations inside the Vue components

You also have an option to add translations directly from within the .vue files. These translations are mostly used for component-specific locales, which is totally fine for smaller applications. However, as your app grows over time, it will become quite difficult to maintain. The component size growth will lead to duplicate translations all over the place. Equally important, as you would most probably let translators take care of string translation – who most likely have no knowledge of Vue components – it is not ideal to have translation inside Vue components.

<i18n> 
{   
  "en": {     
    "hello": "hello!"   
  },   

  "fr": {     
    "hello": "Bonjour!"   
   } 
} 
</i18n> 
<template>
</template>

External JSON or JS Files

We can have separate files for each language. It is more like a key-value pair file. Each language will have the same key corresponding to a particular translation. This is the most commonly used method to recommend and easy to maintain. This will not only prevent duplications as we can reference previous translations but also make proper JSON grouping based on features, etc.

//en.json 
{   
  "message": "Hello", 
  "login": {
    "username": "User Name",
    "password": "Password"
  }
}

Installing Packages

Let’s install nuxt-i18n and configure it.

npm i nuxt-i18n

Since Nuxt does not have a CLI to automatically generate a configuration like vue-cli for vue-i18n, we have to add the following configuration to set up the plugin.

Let us create a config folder and our configuration in the i18n.js file.

import en from '../locales/en.json'
import fr from '../locales/fr.json'
export default {
  locale: 'en',
  fallbackLocale: 'en',
  messages: { en, fr }
}

In this configuration, we are using the external files to store our translation texts. For example, let us use English and French. We can all pass a default and fallback locale to the plugin. In the next step, we create these two external files to store our translations.

Create a locales directory in the root folder and two files, en.json and fr.json, which will contain all our translations.

{
  "message": "Hello!"
}
{
  "message": "Bonjour"
}

Finally, let’s add this library to our nuxt.config.js file so that Nuxt is aware of it. Now we can make use of it in our application.

import i18n from './config/i18n'
 
buildModules: [
 /* other modules */
 [
  'nuxt-i18n',
  vueI18nLoader: true,
  {
    defaultLocale: 'fr',
     locales: [
      {
         code: 'en',
         name: 'English'
      },
      {
         code: 'fr',
         name: 'Français'
      }
    ],
    vueI18n: i18n
  }
 ]
]

vueI18nLoader is optional. This will enable us to write translations in our Vue components. Let us check if the packages are functional: open pages/index.vue, and update the title as shown below:

<template>
  <div class="container">
      <h1 class="title">{{ $t('message') }}</h1>
      <!-- some code -->
  </div>
</template>

Here, $t() tells our Nuxt app to use the translation version of the key based on the selected language. Run the app again, and you can see our English message on the screen!

Let’s manually update the defaultLocale in our nuxt.config.js file to fr:

[
  'nuxt-i18n',
  {
    defaultLocale: 'fr',
     locales: [
      {
         code: 'en',
         name: 'English'
      },
      {
         code: 'fr',
         name: 'Français'
      }
    ],
    vueI18n: i18n
  }
]

We can now see our French message displayed.

Our setup is almost done. Since we are manually updating the code, it becomes tedious to change the locale every time. Let us create a dropdown to choose the language.

Create a components/LanguageInput.vue component with the following code:

<template>
  <div class="lang-dropdown">
    <select v-model="$i18n.locale">
      <option
        v-for="lang in $i18n.locales"
        :key="lang.code"
        :value="lang.code"
        >{{ lang.name }}</option
      >
    </select>
  </div>
</template>

<script>
export default {}
</script>

Let’s add this component to our pages/Index.html  file. Now we can easily switch between languages. Great! Let us deep dive into different ways of localization techniques the library offers and see an example for each one of them.

Named Formatting

There are instances where not every word would need to be translated, such as proper nouns (first or family names, places), URLs, etc. To avoid duplicating them all in every locale file, we can apply the name-formatting technique.

{
  "copyrightMessage": "Copyright: All Rights Reserved { name }"
}

We can use it in our components as follows:

<p>{{ $t('copyrightMessage', { name: 'Phrase.com' }) }}</p>

If we have multiple text pieces that do not need translation, we can also use the list method.

{
  "welcomeMessage": "Welcome! {0}, Your Email: {1} has been verified successfully"
}
<p>{{ $t('copyrightMessage', ['Preetish HS', 'contact@preetish.in']) }}</p>

As each language has its own specific structure, this technique gives the translator the flexibility to place any value anywhere in a sentence.

Pluralization

It is oftentimes 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 need 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 pluralization rules.

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

Number localization

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 file numberFormats.js. Here we can define all our number formats.

export const numberFormats = {
  en: {
    currency: {
      style: 'currency',
      currency: 'USD'
    }
  },
  'en-IN': {
    currency: {
      style: 'currency',
      currency: 'INR'
    }
  },
  fr: {
    currency: {
      style: 'currency',
      currency: 'EUR'
    }
  }
}

Here, based on Intl.NumberFormat, the library will automatically fetch the currency format and symbol for us. Now we need to let our library know that we need to use these formats. Let us add this file to our setup method.

import { numberFormats } from '../formats/numberFormats'
import en from '../locales/en.json'
import fr from '../locales/fr.json'
export default {
  defaultLocale: 'en',
  fallbackLocale: 'en',
  numberFormats,
  messages: { en, fr }
}

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 has to be defined in the numberFormats.js file so that it can work.

<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 symbol and proper formatting, International number system for Dollars (USD) and Indian number system for Rupees (INR).

Date and time localization

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 short, long, reportDateFormat, 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 corresponding values show how that property will be displayed.

Reference: ECMA-International

export const dateTimeFormats = {
  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
    }
  }
}

To use these formats in our application, we use the $d() notation. Just like the number formats, if we don’t explicitly pass the locale, the currently selected locale will be used. If the latter doesn’t have a format defined, then the fallback language format will be used. So make sure to have formats defined for 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.

Nuxt-specific localization

The advantage of using nuxt-i18n over the regular vue-i18n are its additional options to localize routes, as well as a number of SEO-related benefits.

Localizing routes

Just by seeing our URL, we won’t come to know in which language the application is currently used. We are dependent on local storage or cookies to save our locale preferences.

To understand this better, we need to create two more pages, About.vue and Contact.vue, and add the links to those pages to our index.vue page.

<template>
  <div>
    <nuxt-link to="/about">{{ $t('about') }}</nuxt-link>
    <nuxt-link to="/contact">{{ $t('contact') }}</nuxt-link>
  </div>
</template>

Now, if we navigate to the about page, irrespective of the language choice, it opens localhost:3000/about. It would be much better to have a locale prefixed on our URL (localhost:3000/fr/about). To achieve this, there are two strategies:

  • Prefix without default: Our default language,en, will not be prefixed, whereas other languages will, e.g. for French, the about page will have the URL:localhost:3000/fr/.
  • Prefix: With this strategy, all the languages including our default language will be prefixed.
  • Prefix and default: This strategy combines the previous two strategies. i.e. the default language will have both a prefixed and non-prefixed version.

By default, nuxt-i18n would already create prefixed routes for us. Without any additional code, it would apply the third strategy.

//routes generated
[
 {
    path: "/",
    component: _3237362a,
    name: "index___en"
  },
  {
    path: "/fr/",
    component: _3237362a,
    name: "index___fr"
  },
  {
    path: "/about",
    component: _71a6ebb4,
    name: "about___en"
  },
  {
    path: "/fr/about",
    component: _71a6ebb4,
    name: "about___fr"
  },
 {
    path: "/contact",
    component: _71a6ebb4,
    name: "about___en"
  },
  {
    path: "/fr/contact",
    component: _71a6ebb4,
    name: "about___fr"
  }
]

If we navigate to localhost:3000/fr/about, it would automatically load French translations on our about page. However, this will not happen if we navigate through <nuxt-links>. To make this work, we pass a localePath() function with the relevant page name. localePath() is provided by the nuxt-i18n library, which returns the localized path of the passed in route.

<nuxt-link :to="localePath('about')">{{ $t('about') }}</nuxt-link>
<nuxt-link :to="localePath('contact')">{{ $t('contact') }}</nuxt-link>

Default translations will work as it as, when you change the translation to French and then click on about it will now open localhost:3000/fr/about. the about page

If the default language is selected, the route path will remain unchanged. Clicking on about will open to localhost:3000/about. If you now change the translation to French and then click on about, it will open to localhost:3000/fr/about.

To have the prefix “lang” for our default language, we need to pass a strategy attribute to our Nuxt configuration.

// nuxt.config.js

['nuxt-i18n', {
  strategy: 'prefix'
}]

Now load localhost:3000, and it would automatically redirect to localhost:3000/en. If you want to remove the prefixed URLs for the default language, set the strategy to prefix_except_default. This will make the URLs, such as localhost:3000/en, non-functional.

Search Engine Optimization (SEO)

If the seo attribute is set to true during initialization, the library performs various SEO improvements. We also need to pass ISO for each locale, which the library uses to perform these optimizations:

  1. Updating the lang attribute in HTML tag
  2. hreflang generation for anchor tags <a href="" hreflang="<selected-locale-iso>">
  3. Open graph locale tags such as og:locale and og:locale:alternate

Let us update our plugin initialization to get these optimizations automatically made for us. This will wrap up our Nuxt.js tutorial.

[
   'nuxt-i18n',

   {
     strategy: 'prefix',
     defaultLocale: 'en',
     seo: true,
     locales: [
       {
         code: 'en',
         name: 'English',
         iso: 'en-US'
       },
       {
         code: 'fr',
         name: 'Français',
         iso: 'fr-FR'
       }
     ],
     vueI18n: i18n
   }
 ]

Conclusion

Internationalization with nuxt-i18n will make your Nuxt app accessible in any corner of the word. I sincerely hope this Nuxt.js tutorial could help you kick it off right. To learn more techniques for localizing Vue/Nuxt apps, visit the official vue-i18n docs and nuxt-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!

4.2 (84%) 5 votes
Comments
close

The Biggest Mistakes to Watch Out For in Localization

Download our FREE INFOGRAPHIC for a strong overview of the crucial mistakes you need to avoid to ensure your localization process has the best outcome possible.