Development · July 2018

i18n with Gatsby

Taking advantage of Gatsby nodes to add internationalisation to a Gatsby project.

Pedro BrandãoFounder & CEO

Nowadays, the world is our audience. When building a global product for multiple markets, Internationalisation is essential and must be catered from the get-go. English might be a safe bet for most, but there’s undeniable power in serving people content in their native language.

Coming from a Middleman background we honestly were a bit puzzled on how scattered i18n support with Gatsby was: There are some issues at Github, a plugin that requires us to duplicate code and a semi-official blog post with an approach that doesn’t statically render the strings into the html, which IMHO defeats the purpose of using a Static Site Generator. When you build, you’ll just get empty strings that get populated when the JS kicks in. Overall, adding i18n just wasn’t as smooth as it should be.

This quick article gives an alternate approach to i18n with Gatsby for those of us who need all the content to be statically rendered. It also provides two examples on how to use it: one with no external dependencies (except for gatsby source and transformer plugins) and one with React-Intl.

Creating pages for each locale

There’s one fundamental step to achieve static rendering: duplicating each page for each locale.

In this example, we’ll assume that the default language doesn’t require language in the path (i.e. If we have English (default) and Portuguese, we’ll need /about and /pt/about).

It’s always a good idea to centralize the locales definition, so create a locales file somewhere:

1// src/constants/locales.js
3module.exports = {
4 en: {
5 path: 'en',
6 locale: 'English',
7 default: true
8 },
9 pt: {
10 path: 'pt',
11 locale: 'Português'
12 }

Then, in gatsby-node.js let’s create a page for each locale:

1const locales = require('./src/constants/locales')
3exports.onCreatePage = ({ page, actions }) => {
4 const { createPage, deletePage } = actions
6 return new Promise(resolve => {
7 deletePage(page)
9 Object.keys(locales).map(lang => {
10 const localizedPath = locales[lang].default
11 ? page.path
12 : locales[lang].path + page.path
14 return createPage({
16 path: localizedPath,
17 context: {
18 locale: lang
19 }
20 })
21 })
23 resolve()
24 })

For each page, we’re basically just deleting it (so we can take over) and creating it again for each language passing the locale to the page context.

Now, if you go to http://localhost:8000/___graphql and query the site’s pages you’ll see a duplicate for each locale:

Graphql playground

Honestly, that’s pretty much it. Now you can use thepageContext‘slocalein any way you like. 🎉

Example A — with React-Intl

React-Intl is a very powerful i18n package with cool helpers for date relatives, currencies and whatnot. It’s my go-to package for internationalisation so this example will make use of it.

Each page will receive the pageContext object. The main idea is to get the locale from that object and pass it to a Layout component that takes care of creating and feeding the Provider.

First, create some string files. In my case they’ll be src/i18n/en.json

2 "hello": "Hello world"

and src/i18n/pt.json:

2 "hello": "Olá mundo"

Finally, the Layout Component:

1// src/components/Layout.js
3import React from 'react'
4import { IntlProvider, addLocaleData } from 'react-intl'
6// Locale data
7import enData from 'react-intl/locale-data/en'
8import ptData from 'react-intl/locale-data/pt'
10// Messages
11import en from '../i18n/en.json'
12import pt from '../i18n/pt.json'
14const messages = { en, pt }
16addLocaleData([...enData, ...ptData])
18const Layout = ({ locale, children }) => (
19 <IntlProvider locale={locale} messages={messages[locale]}>
20 {children}
21 </IntlProvider>
24export default Layout

All that’s left is to simply use it in the pages:

1import React from 'react'
2import { FormattedMessage } from 'react-intl'
4import Layout from '../components/Layout'
6const IndexPage = ({ pathContext: { locale } }) => (
7 <Layout locale={locale}>
8 <FormattedMessage id="hello" />
9 </Layout>
12export default IndexPage

What about links?

Sure, if we use Gatsby’s Link as-is, we’ll have some trouble navigating between pages. As the URL is our source-of-truth for the locale, if we simply link some page to /about we’ll always be directed to the English about.

We need to make a wrapper around Link, so we can still use <Link to="/about"> as usual, without worrying about the current locale.

Luckily, React-Intl has a very convenient injectIntl HOC that provides us with the intl object (that, unsurprisingly, contains a locale key).

1import React from 'react'
2import { Link } from 'gatsby'
3import { injectIntl, intlShape } from 'react-intl'
5import locales from '../constants/locales'
7const LocalizedLink = ({ to, intl: { locale }, ...props }) => {
8 const path = locales[locale].default ? to : `/${locale}${to}`
10 return <Link {...props} to={path} />
13export default injectIntl(LocalizedLink)

Example B— with no external dependencies

Sometimes you don’t need React-Intl’s power (or don’t want to add yet another dependency bloating the project).

The bare and simplest way to load some strings would be something like

1import React from 'react'
2import enMessages from '../data/index/en.json'
3import ptMessages from '../data/index/pt.json'
4const messages = {
5 en: enMessages,
6 pt: ptMessages
8const IndexPage = ({ pathContext: { locale } }) => (
9 <div>
10 <h1>{messages[locale].hello}</h1>
11 </div>
13export default IndexPage

But in this example we’ll use Gatsby’s data layer to query the content with graphql.

Unfortunately, and even though I’m using Gatsby V2 in these examples, it appears that the new StaticQuery component doesn’t support query variables, so there’s a big drawback with this approach: You’ll have to query everything in the pages and pass it down to components.

Let’s assume we have a folder at src/data. There, we’ll organize the content per folder with JSON files inside (e.g. src/data/index/en.json).

First thing we need to do is to add two gatsby dependencies:

1yarn add gatsby-source-filesystem gatsby-transformer-json

and add them to your plugin configuration in gatsby-config.js

1plugins: [
2 // your plugins
3 'gatsby-transformer-json',
4 {
5 resolve: `gatsby-source-filesystem`,
6 options: {
7 name: `data`,
8 path: `${__dirname}/src/data/`
9 }
10 }

Page’s context data is also available as query variables, so just use that to filter graphql’s results on the page query:

1import React from 'react'
2import { graphql } from 'gatsby'
4const IndexPage = ({ data }) => (
5 <div>
6 <h1>
7 {data.file.childIndexJson.hello}
8 </h1>
9 </div>
12export const query = graphql`
13 query Home($locale: String) {
14 file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) {
15 childIndexJson {
16 hello
17 }
18 }
19 }
22export default IndexPage

Now that we’re using Gatsby’s data layer, we can even take advantage of its powerful transformation features. We can, for instance, load different images per-language and still be able to use gatsby-image:

1import React from 'react'
2import { graphql } from 'gatsby'
3import Img from 'gatsby-image'
5const AboutPage = ({ data }) => {
6 const { image } = data.file.childAboutJson
7 return (
8 <div>
9 <Img resolutions={image.source.childImageSharp.resolutions} />
10 </div>
11 )
14export const query = graphql`
15 query About($locale: String) {
16 file(name: { eq: $locale }, relativeDirectory: { eq: "about" }) {
17 childAboutJson {
18 image {
19 childImageSharp {
20 resolutions(width: 125, height: 125) {
21 ...GatsbyImageSharpResolutions
22 }
23 }
24 }
25 }
26 }
27 }
30export default AboutPage

You can also create a Layout component that accepts the locale (same as above) but then creates a React Context and exports its Consumer. Implementing a LocalizedLink with that would be trivial. Take a look at the repo in the end of the article for an example.


Even though Gatsby doesn’t support i18n out-of-the-box, its powerful lifecycle APIs make it really easy to implement it (in less than 5 minutes).

All the examples here were quickly built for the purpose of the article and should be adapted to your project’s reality. Understanding Gatsby’s nodes and the process of duplicating pages for each locale is, after all, the fundamental of this approach.

Take a look at the repo with examples built using both approaches:


Pedro Brandão

Founder & CEO @ Significa

Pedro el patron Brandão is the founder and CEO at Significa. Pedro’s playlist is made entirely of songs no one has ever listened to.