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.
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
If this article helped you in some way, consider donating. This will help me to create more content like this!
Top comments (5)
Great post 👍🏻
I believe you can hand over all route handling to the plugin's
<Link />
component which does it automatically.Great article. I found now that the function changeLocale can be used to switch languages too.
I was just looking for this topic. Thanks a lot, very interesting.
Are they any options to auto translate the page via Google Translate API?
Nope. I just made a simple base where you can do the translation yourself.