Multi-language (i18n) Gatsby app with Hooks
Tags: gatsby, i18n, tutorial, hooks
Published: 2021-03-06 09:53:28

Recently, working on a Gatsby.js project, I had to implement a multi-language (_internationalization / i18n_) support. A lot of guides are out there, using different approaches/tools, but in this article, I want to show the solution I ended up using and go through the problems that I’ve faced.

_So what do we need to implement and what we want to achieve?_

- Keep configuration and translations in a centralized way.
- Generate language-specific static pages.
- Localize URLs.
- Identify user locale and update components.
- Implement language switcher buttons to alter between the languages.
- Use no external dependencies (or as few as possible).

---

> **Take a look at the [Github repo](https://github.com/ryuuto829/gatsby-i18n) with example project and the [DEMO](https://ryuuto829.github.io/gatsby-i18n/)**

---

## 🌟 Generate pages for each locale

Before we jump into code, we need to think about localizing URLs.  In this example, let's assume that the default language should have no prefix in the URL. We'll add **English** as a _default_ language and **Japanese** as a second one.

```text
╔════════════════════════════════════════════════════╗
║       Languages     ║  index.js  ║   page-2.js     ║
╠════════════════════════════════════════════════════╣
║ English (default)   ║    /       ║   /page-2       ║
║ Japanese            ║    /ja     ║   /ja/page-2    ║
╚════════════════════════════════════════════════════╝
```

Let’s create a module in the `locales.js` file that would contain all locales definitions. _Note that only one language should have the `default: true` key_.

```js
// i18n/locales.js

module.exports = {
  en: {
    path: 'en',
    locale: 'English',
    default: true,
  },
  ja: {
    path: 'ja',
    locale: '日本語',
  },
};
```

Instead of manually writing different components for each language, we can add the following content to our [`gatsby-node.js`](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/) file, where [`onCreatePage`](https://www.gatsbyjs.com/docs/creating-and-modifying-pages/#creating-pages-in-gatsby-nodejs) hook will create a page for each locale and pass its `locale` and `isDefault` props to the page context:

```js
// gatsby-node.js

const locales = require('./i18n/locales');

exports.onCreatePage = ({ page, actions }) => {
  const { createPage, deletePage } = actions;

  // For each page, we’re deleting it, than creating it again for each
  // language passing the locale to the page context
  return new Promise(resolve => {
    deletePage(page);

    Object.keys(locales).map(lang => {
      const isDefault = locales[lang].default || false;

      const localizedPath = isDefault
        ? page.path
        : locales[lang].path + page.path;

      return createPage({
        ...page,
        path: localizedPath,
        context: {
          locale: lang,
          isDefault,
        },
      });
    });

    resolve();
  });
};
```

If you go to `http://localhost:8000/___graphql` and query all pages you should see automatically generated pages for each locale:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjui0fahx9ze9ihb6xrk.png)

Now you can pass [`pageContext`](https://www.gatsbyjs.com/docs/creating-and-modifying-pages/#pass-context-to-pages) to the layout component to be able to use  `locale` and `isDefault` later.

```text
{
  allSitePage {
    edges {
      node {
        path
        context {
          isDefault
          locale
        }
      }
    }
  }
}
```

---

## 🌟 Add translations in JSON format

Let’s place some data in the `i18n/translations/` folder using JSON format  following the defined language conventionon - adding suffix to the filename that contain the language code `[name].[language].json`, like `data.en.json` or `data.js.json`.

First, we need to be sure that our project includes these dependencies:
-   [gatsby-transformer-json](https://www.npmjs.com/package/gatsby-transformer-json) - to get content from JSON files.
-   [gatsby-source-filesystem](https://www.npmjs.com/package/gatsby-source-filesystem) - to query files inside `/i18n/` folder.

```shell
yarn add gatsby-source-filesystem gatsby-transformer-json
```

and then update `gatsby-config.js` configuration:

```js
// gatsby-config.js

module.exports = {
  // ...
  plugins: [
    'gatsby-transformer-json',
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `i18n`,
        path: `${__dirname}/i18n`,
      },
    },
  // ...
  ],
};
```

Let’s create some translations for both languages:

```js
// i18n/translations/en.json
{
  "title": "Gatsby Default Starter",
  "greeting": "Hi people",
  // ...
}

// i18n/translations/ja.json
{
  "title": "Gatsbyデフォルトスターター",
  "greeting": "皆さん、こんにちは",
  // ...
}
```

---

## 🌟 useLocale hook for keeping the current language

Gatsby uses [`@reach/router`](https://www.gatsbyjs.com/docs/reach-router-and-gatsby/) under the hood, so we can get access to the current `pathname` to be able to extract a language name from the URL and use it on app initialization, so we don't need to store any initial data in the _localStorage_ or elsewhere.

> We can take advantage of react's [Context API](https://reactjs.org/docs/context.html) to share current language value between components.

```js
// src/hooks/useLocale.js

import React, { createContext, useState, useContext } from 'react';
import { useLocation } from '@reach/router';

import allLocales from '../../i18n/locales';

const LocaleContext = createContext('');

const LocaleProvider = ({ children }) => {
  const { pathname } = useLocation();

  // Find a default language
  const defaultLang = Object.keys(allLocales)
    .filter(lang => allLocales[lang].default)[0];

  // Get language prefix from the URL
  const urlLang = pathname.split('/')[1];

  // Search if locale matches defined, if not set 'en' as default
  const currentLang = Object.keys(allLocales)
    .map(lang => allLocales[lang].path)
    .includes(urlLang)
    ? urlLang
    : defaultLang;

  const [locale, setLocale] = useState(currentLang);

  const changeLocale = lang => {
    if (lang) {
      setLocale(lang);
    }
  };

  return (
    <LocaleContext.Provider value={{ locale, changeLocale }}>
      {children}
    </LocaleContext.Provider>
  );
};

const useLocale = () => {
  const context = useContext(LocaleContext);

  if (!context) {throw new Error('useLocale must be used within an LocaleProvider');}

  return context;
};

export { LocaleProvider, useLocale };
```

Our hook returns `LocaleProvider`, so we can add it to wrap our layout component:

```js
// src/components/app.js

import { LocaleProvider } from '../hooks/useLocale';
import Layout from './layout';

const App = ({ children, pageContext: { locale, isDefault } }) => (
  <LocaleProvider>
    <Layout locale={locale} isDefault={isDefault}>
      {children}
    </Layout>
  </LocaleProvider>
);
// ...
```

And then, we can use `changeLocale` function to provide a way to change the language using buttons. Adding next code we can be sure that a new value will be stored every time the `locale` value changes:

```js
// src/components/layout.js

import { useLocale } from '../hooks/useLocale';

const Layout = ({ children, pageContext: { locale, isDefault } }) => {
  const { changeLocale } = useLocale();

  // Every time url changes we update our context store
  useEffect(() => {
    changeLocale(locale);
  }, [locale]);
  // ...
};
```
---

## 🌟 useTranslation hook for querying data

There’s a drawback with the approach of using `useStaticQuery` - static queries [do not take variables](https://www.gatsbyjs.com/docs/static-vs-normal-queries/), so we need to manually write all query nodes.
We'll create a helper function to first simplify query response and then filter by current language.

```js
// src/hooks/useTranslation.js

import { useStaticQuery, graphql } from 'gatsby';
import { useLocale } from './useLocale';

const query = graphql`
  query useTranslations {
    allFile(filter: {relativeDirectory: {eq: "translations"}}) {
      edges {
        node {
          name
          childrenTranslationsJson {
            greeting
            mainPageContent
            secondPageLink
            title
            goHomeLink
            secodPageContent
            secondGreeting
            indexPageTitle
            secondPageTitle
            NotFoundPageTitle
            NotFoundPageContent
          }
        }
      }
    }
  }
`;

// This hook simplifies query response for current language.
const useTranslation = () => {
  const { locale } = useLocale();
  const { allFile } = useStaticQuery(query);

  // Extract all lists from GraphQL query response
  const queryList = allFile.edges.map(item => {
    const currentFileTitle = Object.keys(item.node).filter(
      item => item !== 'name',
    )[0];

    return {
      name: item.node.name,
      ...item.node[currentFileTitle][0],
    };
  });

  // Return translation for the current locale
  return queryList.filter(lang => lang.name === locale)[0];
};

export default useTranslation;
```

Now we can use our translations from the `i18n/translations/` data in our components:

```js
// src/pages/page-2.js

// ...
import SEO from '../components/seo';
import useTranslation from '../hooks/useTranslation';

const SecondPage = ({ pageContext }) => {
  const {
    secodPageContent,
    goHomeLink,
    secondGreeting,
    secondPageTitle,
  } = useTranslation();

  return (
    <>
      <SEO title={secondPageTitle} />
      <h1>{secondGreeting}</h1>
      <p>{secodPageContent}</p>
	  
      {/* ... */}
    </>
  );
};
// ...
```

---

## 🌟 Closing Notes

Finally, we'll need to allow the user to correctly browse the site and alternate between both languages. That should be done using localized links and button switcher. You can view them all in this [Github repo](https://github.com/ryuuto829/gatsby-i18n).

Our published project using [Gatsby's default starter](https://github.com/gatsbyjs/gatsby-starter-default)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gc6ywol3qi4niijoruqc.gif)

The i18n implementation we end up with uses no external dependencies (or as few as possible): Context API, hooks, Gatsby’s `createPage()` in the `gatsby-node.js` to generate pages for each locale, `gatsby-transformer-json` plugin to manage translation data.

---

> **Take a look at the [Github repo](https://github.com/ryuuto829/gatsby-i18n) with example project and the [DEMO](https://ryuuto829.github.io/gatsby-i18n/)**

---

## Links
- [Gatsby's default starter]( https://www.gatsbyjs.com/starters/gatsbyjs/gatsby-starter-default/)
- [@reach/router and Gatsby](https://www.gatsbyjs.com/docs/reach-router-and-gatsby/)
- [Multilingual Gatsby.js](https://kalinchernev.github.io/multilingual-gatsbyjs)
- [Static vs Normal Queries](https://www.gatsbyjs.com/docs/static-vs-normal-queries/)

**🎉 Thanks for reading :)**