elcome back to “Gatsby Headaches and How to Cure Them,” a series of articles where we solve the most common challenges that arise when developing with Gatsby. Today, we are in Part 2 on implementing internationalization (i18n) on a Gatsby website, an essential feature on any multilingual website that can be quite a bit of trouble if incorrectly implemented, creating more problems than it solves. In this case, we will see how to add i18n to a Gatsby site without resorting to any plugins.
we peeked at how to add i18n to a Gatsby blog using a motley set of Gatsby plugins. They are great if you know what they can do, how to use them, and how they work. Still, plugins don’t always work great together since they are often written by different developers, which can introduce compatibility issues and cause an even bigger headache. Besides, we usually use plugins for more than i18n since we also want to add features like responsive images, Markdown support, themes, CMSs, and so on, which can lead to a whole compatibility nightmare if they aren’t properly supported.
How can we solve this? Well, when working with an incompatible, or even an old, plugin, the best solution often involves finding another plugin, hopefully one that provides better support for what is needed. Otherwise, you could find yourself editing the plugin’s original code to make it work (an indicator that you are in a bad place because it can introduce breaking changes), and unless you want to collaborate on the plugin’s codebase with the developers who wrote it, it likely won’t be a permanent solution.
But there is another option!
The Solution: Make Your Own Plugin!
Sure, that might sound intimidating, but adding i18n from scratch to your blog is not so bad once you get down to it. Plus, you gain complete control over compatibility and how it is implemented. That’s exactly what we are going to do in this article, specifically by adding i18n to the starter site — a cooking blog — that we created together in Part 1.
The Starter
You can go ahead and see how we made our cooking blog starter in Part 1 or get it from GitHub.
This starter includes a homepage, blog post pages created from Markdown files, and blog posts authored in English and Spanish.
What we will do is add the following things to the site:
- Localized routes for the home and blog posts,
- A locale selector,
- Translations,
- Date formatting.
Let’s go through each one together.
Create Localized Routes
First, we will need to create a localized route for each locale, i.e., route our English pages to paths with a /en/ prefix and the Spanish pages to a path with a /es/ prefix. So, for example, a path like my-site.com/recipes/mac-and-cheese/ will be replaced with localized routes, like my-site.com/en/recipes/mac-and-cheese/ for English and my-site.com/recipes/es/mac-and-cheese/ for Spanish.
In Part 1, we used the gatsby-theme-i18n plugin
 to automatically add localized routes for each page, and it worked 
perfectly. However, to make our own version, we first must know what 
happens underneath the hood of that plugin.
What gatsby-theme-i18n does is modify the createPages process to create a localized version of each page. However, what exactly is createPages?
How Plugins Create Pages
When running npm run build in a fresh Gatsby site, you will see in the terminal what Gatsby is doing, and it looks something like this:
As you can see, Gatsby does a lot to ship your React components into static files. In short, it takes five steps:
- Source the node objects defined by your plugins on gatsby-config.jsand the code ingatsby-node.js.
- Create a schema from the nodesobject.
- Create the pages from your /src/pageJavaScript files.
- Run the GraphQL queries and inject the data on your pages.
- Generate and bundle the static files into the public directory.
And, as you may notice, plugins like gatsby-theme-i18n intervene in step three, specifically when pages are created on createPages:
How exactly does gatsby-theme-i18n access createPages? Well, Gatsby exposes an onCreatePage event handler on the gatsby-node.js to read and modify pages when they are being created.
Learn more about creating and modifying pages and the Gatsby building process over at Gatsby’s official documentation.
Using onCreatePage
The createPages process can be modified in the gatsby-node.js file through the onCreatePage API. In short, onCreatePage is a function that runs each time a page is created by Gatsby. Here’s how it looks:
It takes two parameters inside an object:
- pageholds the information of the page that’s going to be created, including its context, path, and the React component associated with it.
- actionsholds several methods for editing the site’s state. In the Gatsby docs, you can see all available methods. For this example we’re making, we will be using two methods:- createPageand- deletePage, both of which take a- pageobject as the only parameter and, as you might have deduced, they create or delete the page.
So, if we wanted to add a new context to all pages, it would translate to deleting the pages being created and replacing them with new ones that have the desired context:
Creating The Pages
Since
 we need to create English and Spanish versions of each page, it would 
translate to deleting every page and creating two new ones, one for each
 locale. And to differentiate them, we will assign them a localized 
route by adding the locale at the beginning of their path.
Let’s start by creating a new gatsby-node.js file in the project’s root directory and adding the following code:
Note: Restarting the development server is required to see the changes.
Now, if we go to http://localhost:8000/en/ or http://localhost:8000/es/,
 we will see all our content there. However, there is a big caveat. 
Specifically, if we head back to the non-localized routes — like http://localhost:8000/ or http://localhost:8000/recipes/mac-and-cheese/
 — Gatsby will throw a runtime error instead of the usual 404 page 
provided by Gatsby. This is because we deleted our 404 page in the 
process of deleting all of the other pages!
Well, the 404 page wasn’t exactly deleted because we can still access it if we go to http://localhost:8000/en/404 or http://localhost:8000/es/404.
 However, we deleted the original 404 page and created two localized 
versions. Now Gatsby doesn’t know they are supposed to be 404 pages.
To solve it, we need to do something special to the 404 pages at onCreatePage.
Besides a path, every page object has another property called matchPath
 that Gatsby uses to match the page on the client side, and it is 
normally used as a fallback when the user reaches a non-existing page. 
For example, a page with a matchPath property of /recipes/* (notice the wildcard *) will be displayed on each route at my-site.com/recipes/
 that doesn’t have a page. This is useful for making personalized 404 
pages depending on where the user was when they reached a non-existing 
page. For instance, social media could display a usual 404 page on my-media.com/non-existing but display an empty profile page on my-media.com/user/non-existing. In this case, we want to display a localized 404 page depending on whether or not the user was on my-site.com/en/not-found or my-site.com/es/not-found.
The good news is that we can modify the matchPath property on the 404 pages:
This solves the problem, but what exactly did we do in matchpath? The value we are assigning to the matchPath is asking:
- Is the page path /404/?- No: Leave it as-is.
- Yes:- Is the locale in English?- Yes: Set it to match any route.
- No: Set it to only match routes on that locale.
 
 
- Is the locale in English?
 
This results in the English 404 page having a matchPath of /*, which will be our default 404 page; meanwhile, the Spanish version will have matchPath equal /es/* and will only be rendered if the user is on a route that begins with /es/, e.g., my-site.com/es/not-found. Now, if we restart the server and head to a non-existing page, we will be greeted with our usual 404 page.
Besides
 fixing the runtime error, doing leave us with the possibility of 
localizing the 404 page, which we didn’t achieve in Part 1 with the gatsby-theme-i18n plugin. That’s already a nice improvement we get by not using a plugin!
Querying Localized Content
Now that we have localized routes, you may notice that both http://localhost:8000/en/ and http://localhost:8000/es/
 are querying English and Spanish blog posts. This is because we aren’t 
filtering our Markdown content on the page’s locale. We solved this in 
Part 1, thanks to gatsby-theme-i18n injecting the page’s locale on the context of each page, making it available to use as a query variable on the GraphQL query.
In this case, we can also add the locale into the page’s context in the createPage method:
Note: Restarting the development server is required to see the changes.
From here, we can filter the content on both the homepage and blog posts, which we explained thoroughly in Part 1. This is the index page query:
And this is the {markdownRemark.frontmatter__slug}.js page query:
Now, if we head to http://localhost:8000/en/ or http://localhost:8000/es/, we will only see our English or Spanish posts, depending on which locale we are on.
Creating Localized Links
However,
 if we try to click on any recipe, it will take us to a 404 page since 
the links are still pointing to the non-localized recipes. In Part 1, gatsby-theme-i18n gave us a LocalizedLink component that worked exactly like Gatsby’s Link but pointed to the current locale, so we will have to create a LocalizedLink component from scratch. Luckily is pretty easy, but we will have to make some preparation first.
Setting Up A Locale Context
For the LocalizedLink
 to work, we will need to know the page’s locale at all times, so we 
will create a new context that holds the current locale, then pass it 
down to each component. We can implement it on wrapPageElement in the gatsby-browser.js and gatsby-ssr.js Gatsby files. The wrapPageElement is the component that wraps our entire page element. However, remember that Gatsby recommends setting context providers inside wrapRootElement, but in this case, only wrapPageEement can access the page’s context where the current locale can be found.
Let’s create a new directory at ./src/context/ and add a LocaleContext.js file in it with the following code:
Next, we will set the page’s context at gatsby-browser.js and gatsby-ssr.js and pass it down to each component:
Note: Restart the development server to load the new files.
Creating LocalizedLink
Now let’s make sure that the locale is available in the LocalizedLink component, which we will create in the ./src/components/LocalizedLink.js file:
We can use our LocalizedLink at RecipePreview.js and 404.js just by changing the imports:
Redirecting Users
As
 you may have noticed, we deleted the non-localized pages and replaced 
them with localized ones, but by doing so, we left the non-localized 
routes empty with a 404 page. As we did in Part 1, we can solve this by 
setting up redirects at gatbsy-node.js to take users to the
 localized version. However, this time we will create a redirect for 
each page instead of creating a redirect that covers all pages.
These are the redirects from Part 1:
These are the new localized redirects:
We
 won’t see the difference right away since redirects don’t work in 
development, but if we don’t create a redirect for each page, the 
localized 404 pages won’t work in production. We didn’t have to do this 
same thing in Part 1 since gatsby-theme-i18n didn’t localize the 404 page the way we did.
Changing Locales
Another vital feature to add is a language selector component to toggle between the two locales. However, making a language selector isn’t completely straightforward because:
- We need to know the current page’s path, like /en/recipes/pizza,
- Then extract the recipes/pizzapart, and
- Add the desired locale, getting /es/recipes/pizza.
Similar
 to Part 1, we will have to access the page’s location information (URL,
 HREF, path, and so on) in all of our components, so it will be 
necessary to set up another context provider at the wrapPageElement function to pass down the location object through context on each page. A deeper explanation can be found in Part 1.
Setting Up A Location Context
First, we will create the location context at ./src/context/LocationContext.js:
Next, let’s pass the page’s location object to the provider’s location attribute on each Gatsby file:
Creating An i18n Config
For
 the next step, it will come in handy to create a file with all our i18n
 details, such as the locale code or the local name. We can do it in a 
new config.js file in a new i18n/ directory in the root directory of the project.
The LanguageSelector Component
The last thing is to remove the locale (i.e., es or en) from the path (e.g., /es/recipes/pizza or /en/recipes/pizza). Using the following simple but ugly regex, we can remove all the /en/ and /es/ at the beginning of the path:
It’s important to note that the regex pattern only works for the en and es combination of locales.
Now we can create our LanguageSelector component at ./src/components/LanguageSelector.js:
Let’s break down what is happening in that code:
- We get our i18n configurations from the ./i18n/config.jsfile instead of theuseLocalizationhook that was provided by thegatsby-theme-i18nplugin in Part 1.
- We get the current locale through context.
- We find the page’s current pathname through context, which is the part that comes after the domain (e.g., /en/recipes/pizza).
- We remove the locale part of the pathname using the regex pattern (leaving just recipes/pizza).
- We
 render a link for each available locale except the current one. So we 
check if the locale is the same as the page before rendering a common 
Gatsby Linkto the desired locale.
Now, inside our gatsby-ssr.js and gatsby-browser.js files, we can add our LanguageSelector, so it is available globally on the site at the top of all pages:
// ./gatsby-ssr.js & ./gatsby-browser.js
import * as React from "react";
import { LocationProvider } from "./src/context/LocationContext";
import { LocaleProvider } from "./src/context/LocaleContext";
import { LanguageSelector } from "./src/components/LanguageSelector";
export const wrapPageElement = ({ element, props }) => {
  const { location } = props;
  const { locale } = element.props.pageContext;
  return (
    <LocaleProvider locale={locale}>
      <LocationProvider location={location}>
        <LanguageSelector />
        {element}
      </LocationProvider>
    </LocaleProvider>
  );
};  Localizing Static Content
The last thing to do would be to localize the static content on our site, like the page titles and headers. To do this, we will need to save our translations in a file and find a way to display the correct one depending on the page’s locale.
Page Body Translations
In Part 1, we used the react-intl package for adding our translations, but we can do the same thing from scratch. First, we will need to create a new translations.js file in the /i18n folder that holds all of our translations.
We will create and export a translations object with two properties: en and es, which will hold the translations as strings under the same property name.
We know the page’s locale from the LocaleContext we set up earlier, so we can load the correct translation using the desired property name.
The cool thing is that no matter how many translations we add, we won’t bloat our site’s bundle size since Gatsby builds the entire app into a static site.
“
Note: Another way we can access the locale property is by using pageContext in the page props.
Page Title Translations
We ought to localize the site’s page titles the same way we localized our page content. However, in Part 1, we used react-helmet for the task since the LocaleContext isn’t available at the Gatsby Head API.
 So, to complete this task without resorting to a third-party plugin, we
 will take a different path. We’re unable to access the locale through 
the LocaleContext, but as I noted above, we can still get it with the pageContext property in the page props.
Formatting
Remember that i18n also covers formatting numbers and dates depending on the current locale. We can use the Intl object from the JavaScript Internationalization API. The Intl
 object holds several constructors for formatting numbers, dates, times,
 plurals, and so on, and it’s globally available in JavaScript.
In this case, we will use the Intl.DateTimeFormat constructor to localize dates in blog posts. It works by creating a new Intl.DateTimeFormat object with the locale as its parameter:
The new Intl.DateTimeFormat and other Intl instances have several methods, but the main one is the format method, which takes a Date object as a parameter.
The format method takes an options object as its second parameter, which is used to customize how the date is displayed. In this case, the options object has a dateStyle property to which we can assign "full", "long", "medium", or "short" values depending on our needs:
In the case of our blog posts publishing date, we will set the dateStyle to "long".
Conclusion
And just like that, we reduced the need for several i18n plugins to a grand total of zero. And we didn’t even lose any functionality in the process! If anything, our hand-rolled solution is actually more robust than the system of plugins we cobbled together in Part 1 because we now have localized 404 pages.
That said, both approaches are equally valid, but in times when Gatsby plugins are unsupported in some way or conflict with other plugins, it is sometimes better to create your own i18n solution. That way, you don’t have to worry about plugins that are outdated or left unmaintained. And if there is a conflict with another plugin, you control the code and can fix it. I’d say these sorts of benefits greatly outweigh the obvious convenience of installing a ready-made, third-party solution.
 
