dois:pontos

Making a multilingual site with Gatsby

- 7 min

After converting a site from Jekyll to Gatsby, one thing was missing: how do I make it bilingual? With Jekyll I already knew how to do it, but not at Gatsby. I looked on several sites for any tips on how to do this, but most of them were tutorials for an integration with some CMS or external services. My need was just a basic one, a simple website with content made in Markdown files.

I didn't find any tutorial that got exactly what I needed, so I had to force myself to find a solution. Fortunately, it worked and this site is proof of that. Below I describe the process I used to achieve this goal.

Gatsby INTL Result

Plugin installation

To add support for other languages on the site, I installed the gatsby-plugin-intl plugin. There are other extensions to achieve the same goal, but this was the one that best served me.

To install with Yarn just use this command in the terminal:

yarn add gatsby-plugin-intl

If you are using NPM, use this other one.

npm install gatsby-plugin-intl

Done. The installation is complete.

Configuration

In Gatsby, after installing a plugin, a configuration is made to include it in the build process. Just include the name of the plugin, along with your options within the list of plugins, in the gatsby-config.js file. Mine was configured as follows:

module.exports = {
  plugins: [
    /* PLUGIN CONFIGURATION */
    {
      resolve: `gatsby-plugin-intl`,
      options: {
        // Directory with the strings JSON
        path: `${__dirname}/src/intl`,
        // Supported languages
        languages: [`pt`, `en`],
        // Default site language
        defaultLanguage: `pt`,
        // Redirects to `/pt` in the route `/`
        redirect: false,
      },
    },
    /* END OF CONFIGURATION */
  ],
}

A brief explanation of the above options:

  • resolve: name of the Gatsby plugin
  • options: list with options for configuration
  • path: path to the directory where the JSON files with the all translation strings are located. The keyword __dirname replaces the need to enter the absolute address of the folder.
  • languages: list with the ISO abbreviations of the desired language for the website. Example: pl for Polish andde for German. In my case, I used only Portuguese and English.
  • defaultLanguage: default language of the website. Portuguese in my case.
  • redirect: add /pt to the URL of the website with default language. I left it as false for my website, so as not to affect the existing addresses.

Terms for translation

In addition to the configuration, you must have a file with the terms to be translated on the website. Link names, static page titles and tooltips are good applications.

{
  "about": "Sobre",
  "comments": "Comentários",
  "home": "Início"
}

In the example above, I used a list, with a term and its equivalent translation. The structure must be the same for all the languages you want to add to your site, just changing the translation, of course.

The file name must follow the [language-iso].json pattern, within the directory mentioned in the configuration.

Example: src/intl/en.json, src/intl/pt.json, etc.

Applying translations to files

After this is done, there comes the part of translating the pages and components. To do this, just follow the steps:

Import the useIntl hook from the installed plugin:

import React from "react"
// Import hook
import { useIntl } from "gatsby-plugin-intl"

export default function Index() {
  // Making useIntl available in the code
  const intl = useIntl()
  // Use language iso for the routes
  const locale = intl.locale !== "pt" ? `/${intl.locale}` : ""

For the translation itself, the word to be translated is replaced by the formatMessage method.

  /* Before */
  <Link activeClassName="active" to="/">
    Início
  </Link>
  /* After */
  <Link activeClassName="active" to={`${locale}/`}>
    {intl.formatMessage({ id: "home" })}
  </Link>

For dates, the component <FormattedDate /> is used.

<FormattedDate value={new Date(postDate)} month="long" day="numeric" />

Documentation for the options available for the component can be found here.

Listing of articles at markdown

A bilingual website does not live only on word translations, but mainly on content. In the example mentioned in this article, it comes from Markdown files in the /posts directory. Nothing much different from normal was done in the gatsby-node.js file.

const path = require("path")

exports.createPages = async ({ actions, graphql, reporter }) => {
  const { createPage } = actions
  const blogPostTemplate = path.resolve("src/templates/blog-post.js")
  const search = await graphql(`
    query {
      allMarkdownRemark(
        sort: { order: DESC, fields: frontmatter___date }
        limit: 1000
      ) {
        edges {
          node {
            frontmatter {
              slug
              lang
            }
          }
        }
      }
    }
  `)

  if (search.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }

  // Context and page template for the content
  search.data.allMarkdownRemark.edges.forEach(({ node }) => {
    const language = node.frontmatter.lang
    const locale = language !== "pt" ? `/${language}` : ""
    createPage({
      path: `/post${node.frontmatter.slug}`,
      component: blogPostTemplate,
      context: {
        slug: node.frontmatter.slug,
        lang: language,
      },
    })
  })

  // Pagination for articles
  const posts = search.data.allMarkdownRemark.edges
  const postsPerPage = 20
  const numPages = Math.ceil(posts.length / postsPerPage)
  Array.from({ length: numPages }).forEach((_, i) => {
    createPage({
      path: i === 0 ? `/articles` : `/articles/${i + 1}`,
      component: path.resolve("./src/templates/articles.js"),
      context: {
        limit: postsPerPage,
        skip: i * postsPerPage,
        numPages,
        currentPage: i + 1,
      },
    })
  })
}

This file is responsible for reading the *.md files and turning them into HTML pages.

First, a query is made in GraphQL to find the data in the markdown files. Then, the template file for the page for the article is associated with its context. The context is what tells Gatsby which file to show when accessing a link.

Finally, the pagination of the list of articles, with 10 items per page. The number twenty appears there because there are ten posts for each language, as the site has 2, I left the postsPerPage as 20. I know it is not the most elegant way out, but it is the one that worked for me. If I find a better one, I update this article and the repository with it.

Markdown content for languages

The front matter, a kind of header for content files, has the structure below:

---
lang: pt
title: "Lorem ipsum"
slug: "/lorem-ipsum"
date: 2020-07-11
categories: lorem
thumbnail: https://lorempixel.com/1500/900
---

## Lorem

Lorem ipsum dolor sit amet consectetuer adispiscing elit.

Nothing special, except language identification, for later filtering. Just place them in the folder informed to receive the files in gatsby-node.js. I was careful to separate them into subdirectories for each language.

Listing the content

To list the articles, I first made a query in GraphQL to bring all the articles, according to the specifications given in the gatsby-node.js file in thecreatePages page creation function.

export const articlesQuery = graphql`
  query articlesQuery($skip: Int!, $limit: Int!) {
    allMarkdownRemark(
      sort: { fields: frontmatter___date, order: DESC }
      limit: $limit
      skip: $skip
    ) {
      edges {
        node {
          id
          excerpt
          frontmatter {
            date
            slug
            title
            lang
          }
        }
      }
    }
  }
`

After that, the query result is used on the page.

import React from "react"
import { graphql, Link } from "gatsby"
import { useIntl } from "gatsby-plugin-intl"

export default function Articles(props) {
  // Internationalization
  const intl = useIntl()
  const locale = intl.locale !== "pt" ? `/${intl.locale}` : ""

  // Raw query data
  const posts = props.data.allMarkdownRemark.edges

  // Filtering posts by locale
  const filteredPosts = posts.filter((edge) =>
    edge.node.frontmatter.lang.includes(intl.locale)
  )

For more details on this file, just visit the repository I made as an example on Github. The link is at the end of this article.

Switching between languages

Nothing special here either:

import React from "react"
import { Link } from "gatsby"

export default function LanguageSelector({ label, className }) {
  const labelText = label || "Languages"
  const selectorClass = className || "language-selector"

  return (
    <div className={selectorClass} data-label={labelText}>
      <ul>
        <li>
          <Link to="/en">En</Link>
        </li>
        <li>
          <Link to="/">Pt</Link>
        </li>
      </ul>
    </div>
  )
}

As the internationalization plugin works based on routes, it was enough to make a link to the route of the desired language. I did this to avoid 404 errors when changing the language on the article's single page, since the URLs of the English and Portuguese versions are different.

Conclusion

It may not be the best strategy for creating multilingual sites, but this was the one that worked for me. As I said at the beginning of this article, it was more difficult than I thought to find any help on this topic. Perhaps because it is already so common for some, they forget that are people starting who still have no idea how to do it.

I left a link to the project repository on Github down below. Feel free to add any suggestions or comments!

Links

Comentários