Adding internationalization (i18n) to a Gatsby project with gatsby-plugin-intl is simple as installing any other Gatsby plugin and configuring it. Once set up, plugin generates a i18n route for every page and for every language that is added in the plugin config.

For example, if we have 10 pages (static routes) and have 3 languages, i18n plugin creates 30 routes (HTML files) total. If the configured languages are en (English), de (German), hr (Croatian) and if we look at the src/pages/about.js route component, we’ll get the following routes when we run or build our project:

  • en/about
  • de/about
  • hr/about


With redirect option enabled in the plugin, Gatsby does default redirects from a non-i18n routes to a default-i18n routes. Meaning that if a user requests an about page without any i18n specified in the URL, they will be redirected to a default i18n route. If the default is English, user will be redirected to  en/about.
This is how the i18n plugin handles static routes (public pages). What about the client-only routes?

Client-only routes


Client-only routes are protected routes that are not directly accessible. Gatsby doesn’t build any static assets for these routes, including HTML files. Client-only routes are useful when building a page that requires authorization or if the page content heavily depends on the API response. These pages only exist on the client-side.
In Gatsby, client-only routes rely on a custom Router that handles authorization. Here is the example of src/app/app.js where a custom Router component is called and client-only routes are defined.

import React from "react"
import { Router } from "@reach/router"
import Login from "../app/login"
import Register from "../app/register"
import Layout from "../components/layout"

const App = () => (
    <Layout>
      <Router>
        <Login path={`/app/login`} />
        <Register path={`/app/register`} />
      </Router>
    </Layout>
  )

export default App

Issue with i18n plugin and client-only routes


Gatsby is aware of the client-only router and will include these client-only pages on build time, but it will only create a JavaScript assets that are needed to build the pages dynamically. Our app.js will handle routing and path matching.
Plugin is not aware that these routes exist, so these routes won’t work out the box with our i18n plugin. Keep in mind that the gatsby-plugin-intl adds i18n prefixes to routes en/, de/, etc. Even though we have added a /app/login and /app/register paths in our router, the real paths have those i18n prefixes.

But we don’t want to manually add those when we creates the sites. We might need to support more languages in the future and we’ll need to add more pages. That is why we want to create them programmatically on build time and let Gatsby take care of it.

Adding i18n support to client-only routes


First we need to instruct Gatsby how to build pages that are included in the client-only routes (in the src/app folder). We’ll do that by adding the following code to gatsby-node.js.

exports.onCreatePage = ({ page, actions }) => {
  const { createPage } = actions
  // notice the addition of .*
  if (page.path.match(/^\/.*\/app/)) {
    // notice page.context.language
    page.matchPath = `/${page.context.language}/app/*`
    createPage(page)
  }
}

And we need to make a slight adjustment to our Router in src/app/app.js component.

import React from "react"
import { Router } from "@reach/router"
import Login from "../app/login"
import Register from "../app/register"
import Layout from "../components/layout"
const App = () => (
  <Layout>
    <Router basepath="/:lang/app">
      <Login path={`/login`} />
      <Register path={`/register`} />
    </Router>
  </Layout>
)
export default App

With this solution, we can add as many new client-only routes and new languages as we want and Gatsby will know how to handle the new routes. All components and paths that are references within this Router component will be included on the build time.

Thank you for taking the time to read this post.  I would like to hear your thoughts on this issue, so let me know if you’ve encountered a similar issue on your project and how you’ve solved it.

Feel free to share your thoughts in the comments or drop us an email at hello@prototyp.digital. 😄