How to approach multi-language Gatsby apps

Aggelos Arvanitakis
ITNEXT
Published in
10 min readMar 28, 2020

--

I recently had to implement a multi-language static website, so I thought Gatsby would be a fun choice; surely people have a lot of guides out there on how to do it. Turns out, implementing it the naive way is easy, but if you want to minify manual work and streamline translations through automated processes, things can get hard. In this article, I’ll be attempting to showcase the problems that I’ve faced, the solutions that I ended up using and the benefits of each approach.

Intro

Initially, internalization might seem like an easy task. You get a request, you check which language is requested, you pre-populate a document with the correct data and you return the HTML back to the browser. What you might forget here, is that Gatsby doesn’t come with a server. Unlike NextJS, Gatsby outputs a collection of static assets that normally get automatically served by a static website host like Netlify or S3 (with website configuration turned on). Manually setting up & configuring a server to serve the proper content, beats the whole purpose of using Gatsby in the first place. Thus, although internalization is considered a “solved” issue in the server-side rendering world, it suddenly becomes interesting in the modern static-file serving world.

What we want to achieve is the following:

  • Output variations of each page for each of the languages that we support.
  • Pre-render each outputted page with the correct language.
  • Add proper SEO internalization tags on each outputted page.
  • Implement a language picker to alter between the languages.
  • Add proper server-side redirects based on the locale of the visiting user.

At the same time, the project should be bound by the following technical requirements:

  • Have a centralized way of handling translations.
  • Automate the process of creating localized versions of each page (i.e. write the code for each page only once)
  • Automate the addition of proper SEO tags to each page.
  • Automate the addition of locale-based server-side redirects.
  • Automate the creation of an intelligent language-picker & language-aware links.

Oh boy, you are in for a treat. Let’s get started!

Centralizing translations

The first thing is deciding how you want to handle translations. Normally, what you do is keep a JSON file(s) with all the variations of phrases or words for your website. Whether you keep a single JSON file for everything or you split it into separate JSON files per page, really depends on the use case. If your website is small enough and supports only a handful of languages, then a single JSON file for everything (even with a footprint of a few hundred KBs) might be the best solution. On the other hand, if you have a site with multiple languages, supporting an arbitrary number of pages (i.e. an online shop with loads of products), then generating multiple translation documents would normally be preferable. Generally, in a React project, you’ve got the following options:

  • Load the single translation document once at the start and then navigate & change languages freely.
  • Lazy-load the translation documents on demand and use <React.Suspense> fallback whenever the request for a translation document is in-flight.

For this article, we’ ll go ahead and use the single-document approach, utilizing the i18next library. We will first configure a translation module (i.e. how to load the documents, what’s the fallback language, how to handle translation requests) and then pass it down the component chain through React.Context. This will contain helpful methods that will allow us to change languages and translate a certain pieces of text.

Let’s create an instance of the i18next library and configure it to use the translations found under translations.json :

Example of a translation document for i18next
Example configuration of i18next

The exported module has the methods that we need in order to change languages or perform translations. Let’s expose it to our React components through a context provider, by installing react-i18next (i.e. the react-binding of i18next) and its I18nextProvider module. The components can then use the useTranslation hook from the same package, in order to access the translation function:

translation example

Perfect, now we have a methodology for centralizing the translations. Let’s see how can we use that within Gatsby.

Automating the page localization

Just because we are outputting static assets, we need to create one HTML document for each page in each available language. Of course, we can approach it like in the old days and manually write different HTML documents for each language, but we are working with modern tools so we should be searching for modern solutions.

In its internalization docs, Gatsby mentions the gatsby-plugin-18n, which converts all src/pages/about.en.js files, to /en/about/ urls. This unfortunately won’t work for us, because it means that we physically need to create as many about.{LOCALE}.js files as the number of our supported languages. What we want, is to write a single src/pages/about.js file and automatically generate /en/about , /el/about , /fr/about for all of our supported languages. This is super easy to achieve by leveraging the Gatsby Node APIs.

What we are going to do, is use the onCreatePage hook to automatically create new localized versions (at the correct path) of every page found in the pages folder. To do that, we add the following content to our gatsby-node.js file, which is located at the top level of our project directory:

Gatsby Node API that helps us dynamically create pages

In the example above, I added the available locales as a key in gatsby-config.js. The value of supportedLanguages would be a simple array of string locales like ['en', 'el', 'fr'].

If you attempt to build your Gatsby project (through gatsby build ), you would see the HTML pages have been created, but the content isn’t translated. This is because we haven’t yet told our pages what language are they implementing nor what thing should they translate. Let’s fix that.

First off, let’s add the <I18NextProvider> in our root component, so that pages can use the useTranslation() hook. To do that, we edit our gatsby-browser.js and gatsby-ssr.js like so:

gatsby-ssr wraps the root element with a provider. gatsby-browser just exports whatever ssr did.

Now pages can const { t } = useTranslation() and translate their text. The missing thing is the language. Each page doesn’t know which language should it translate to. Of course, we can infer that through the path (i.e. /en/about has a language of en), but that’s a bit flimsy and prone to errors. What we will do instead, is make each page aware of its locale by explicitly passing it to it during its creation. In Gatsby, this means passing a context to a page during its creation. This context is an object whose contents are received as props by the page component. Just because we might need it in multiple other components within a page, we will add it to a React.Context in order to read it from any subcomponent.

To achieve that, we will need to modify the previous gatsby-node.js file to the following:

gatsby-node adds top-level props to each generated page

Create a PageContext ( and usePageContext hook) that will pass down these context props to any React component within a page:

A Context that will allow us to pass those top-level props (contained in `pageContext`) to any component down the chain. We also set the correct language here for the entire page & its components. We can’t add the `changeLanguage` in a React effect.

And finally modify the gatsby-ssr.js and gatsby-browser.js to wrap every page with a PageContext.Provider so that we can “read” the data that we passed through our gatsby-node.js file. At the same time, we make sure to set the correct language (since we now know the language of each page) during server-side rendering, so that the t method of useTranslation() can know which language to translate to.

Pass down the `pageContext` props

Automating the process of adding locale-based redirects.

So everything is perfect! Now, each page has its proper translation and everything works great. If you start a development server through gatsby develop , you will see that localhost:8000/ returns a 404, since the / page only exists in its localized versions (i.e. /en/ ). What we should do, is add a redirect from / to the default/fallback locale. In fact, we should do that for every page. Ideally, we would want to redirect users to the proper locale based on their language preferences or fallback to english if we don’t support any of the languages they prefer.

This is a “server-thing” and relates to parsing the Accept-Language header and creating proper redirect rules out of it. Gatsby is not able to handle it alone, since, as mentioned, it has no notion of a server. Interestingly enough though, Gatsby has an API for creating redirects called createRedirect() which we can use inside our gatsby-node.js. We can use this API to “publish” our intent of creating a redirect from one page to another. It’s up to the server-side technology to “subscribe” to these intents and create redirect rules. In short Gatsby says “I’ll log when the developer wants to create a redirect” and someone else must say “I’ll read those logs and create redirect rules”. Plugins like gatsby-plugin-netlify or gatsby-plugin-s3 do just that. The Netlify one creates a _redirects file in your build directory which contains all the redirect rules (as read through your createRedirect calls throughout your gatsby-node.js) in a way that Netlify understands them. We will leverage that to create server-side redirects for the production website, while also creating browser-side javascript redirects for development, so that localhost:8000/about can redirect correctly to localhost:8000/en/about . To do that, we add gatsby-plugin-netlify to gatsby-config.jsand modify our gatsby-node.js to the following:

Adds redirects based on Accept-Language

We now make sure to automatically redirect users to the correct page version based on their locale and fallback to the default language (normally en ) if there was no locale match. Netlify parses the redirects sequentially, so it will only reach the last one (our fallback to the default language) if nothing else matched before that. The output, located at /build/_redirects, would look something like:

/about /en/about/ 301 Language=en
/about /fr/about/ 301 Language=fr
/about /en/about/ 301

If you are wondering why we need server-side redirects and not just browser ones, it’s because we don’t want to rely on custom Javascript code to perform it for us. Users may disable scripting or have poor internet connections, which can lead to a late redirects. In addition, we can’t set a status code for a redirect, thus search engines won’t be able to index this action correctly. Relying on JS for development purposes is fine, but for production it’s a no-no.

Automate the process of adding proper SEO tags to each page.

Each page has its own locale, but unfortunately Google doesn’t know that. Let’s help out a bit, by adding the proper SEO tags in each page. Following the official convention, let’s create an SEO component that will read the locale information through our usePageContext hook that we created above. It will then use react-helmet to add the proper HTML tags to the document (make sure to add gatsby-plugin-react-helmet).

Batteries-included SEO component

The first thing we did, was to query the gatsby-config for the available languages (i.e. a list of locale strings) and the base URL (i.e. https://example.com ) in order to build the necessary <meta> attributes. The next thing, is to get the current language that’s associated with the current localized page, as well as the original path that the page would have had if it was not localized. With this information we:

  • Add a lang attribute to the <html> tag.
  • Generate an og:locale <meta> tag.
  • Add fallbacks for the description <meta> tag, if the user doesn’t submit one for the current page.
  • Add <meta rel="canonical" … /> for the current page.
  • Add <meta rel="alternate" … /> for all localized versions of the current page.

This helps google show the proper localized search results. based on the user’s language preferences. Just make sure that an <SEO /> component is added to all of your pages. We are not automatically including it, since its props may differ from page to page.

Creating an intelligent language-picker component

The last piece of the puzzle is allowing the user to correctly browse the site and alternate between languages.

When the users are english, all the links present in the header should have the /en/ prefix. When they are french, then all the pages in the navigation should be prefixed with /fr/ . That should be done automatically for us, without having to think about locales at all. Let’s create a new Link component that extends the Gatsby one by adding locale prefix to the path, based on the currently active language. That would look something like this:

A lang-aware Link component

Now all we need to do, is use this wherever we would use our original Link component. With this component, we don’t have to worry about locales at all. We just write <Link to="/about/">About</Link> and the proper locale is automatically prefixed for us, ensuring that users always stay on the same locale when browsing around.

Finally, let’s give them the ability to change language. Each localized page has its own URL, thus changing languages is nothing more than changing pages. The trick is that we not only want to automate the creation of this picker, but we also want to make sure that we redirect the users to the same page they were on. For example, if they were on /en/about/ and they change the language to french, we want to redirect them to /fr/about. This means that our language picker should be a set of links whose href attributes change every time the user changes page.

We can utilize our usePageContext to read the originalPath of the current page and create a list of links with the originalPath prefixed by each locale. A style-less implementation would look like this:

A simple auto-generated language picker

With this in place, each new language gets automatically added to the picker, while ensuring that changing a language retains the user to the same page.

Closing Notes

Wow, this was a lot. To sum up, we:

  • Set up a way to automatically generate HTML documents for all locales.
  • Handled server-side redirects in production & client-side redirects in development.
  • Set up the language for each page and passed that down to its subcomponents using React.Context .
  • Set up an <SEO /> component that reads the language of each page and creates the necessary i18n meta tags.
  • Created a useful language-aware <Link> component.
  • Created a dynamic & smart language-picker component to switch languages.

What we also achieved here is a localized development environment that fully mimics that of the production. The purpose of it all, was to automate localization for a Gatsby website, while hiding the complexity of it from the developer & the exposed component APIs. By all means, feel free to suggest alternative options or things that could have made all this far simpler. I would be really interested to know them.

I also published a set of tools that implement all of the above. It includes:

  • A starter to use as boilerplate for new multi-language Gatsby apps
  • A theme (no styling included) to help with migrating existing gatsby apps to multi-language ones
  • A plugin to help when you want to use your own translation framework, but still need the automatic generation of the localized pages (with SEO tags included)

You can view them all here:

https://github.com/3nvi/gatsby-intl

Thanks for reading :)

P.S. 👋 Hi, I’m Aggelos! If you liked this, consider following me on twitter and sharing the story with your friends 😀

--

--