Bruce Röttgers

How to write your own small custom i18n implementation for Gatsby

Last updated on 2020-3-28 | gatsby | react

If you dig this post, you might want to subscribe to my newsletter to receive a new one each week.

While working on a portfolio site, I needed to implement translations for two languages. After considering multiple options, I decided to just write an implementation myself, instead of relying on a Gatsby plugin. My approach I’m going to present to you has great benefits, but is limited to not being very scalable. So if you want to translate your own portfolio website, to english and your native language, this is for you. If you have a big web app, which you’ll probably expand to other languages and rely on advanced i18n implementations, this tutorial is not for you. I would suggest reading through the Gatsby documentation on i18n to find a different implementation that suits you best.

If you do want a quick and clean solution, for your project read on!

The Approach

My approach to this problem was to basically run two sites of the same codebase. During the build progress, I would decide based on an environment variable which language to display. I would then query all translations from a json file and pass them in, wherever I need them.

The Build Process

There are two methods to this, you can either store your language in a .env file to read from it during development, or define multiple npm scripts, which set the variable before running gatsby build or other methods. To keep the code DRY I have decided to use a .env file. To do this you will have to install dotenv with your favourite package manager:

yarn add dotenv

dotenv reads all environment variables from your .env file and adds them to your shell.

After you have installed doting, create a .env file at your project’s root if you do not already have one and add this to it:

LANGUAGE="en"

Now, we are ready to use this in our gatsby-config.js, add the following lines to your config:

// we need to configure `dotenv` at the very top of the project
require("dotenv").config()

// this should go into your config object
siteMetadata: {
    language: process.env.LANGUAGE
 },

Now you will be able to access the language everywhere in your code, via a GraphQL query similar to this:

query {
  site {
    siteMetadata {
      language
    }
  }
}

Note: We need to make the language available via GraphQL, since your environment variable will not be available to you in the components when someone visits your website.

Adding JSON to our project

The easiest way to query or translations, is to do it via GraphQL, as we do with basically all things in Gatsby. To query json files, we need to add two plugins to our project. gatsby-source-filesystem allows you to query files in your filesystem and gatsby-transformer-json will let you access the contents of your .json files via GraphQL. First install them via your favourite package manager:

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

Then, you will need to configure both in your gatsby-config.js file:

// ...
plugins: [
	// ...
	{
      resolve: "gatsby-transformer-json",
      options: {
		// with this option, your translations will be available in GraphQL as `allTranslation`
        typeName: "Translation"
      }
    },
    {
      resolve: "gatsby-source-filesystem",
      options: {
        name: "translations",
        path: `${__dirname}/src/locale`
      }
    }
],
// ...

Next, create a new folder at the location src/locale and create a new file named translations.json in it (or name it differently, it does not make a difference). This file will contain all strings and we will structure it like this:

[
  {
    "key": "some-descriptive-key",
    "de": "This is the version for the language de",
    "en": "This is the version for the language en"
  }
]

With everything created, we can move on to creating hooks and integrating them into our components and pages.

The Hooks

Hooray, React Hooks! Don’t worry, we’re not doing anything advanced

First, we’ll create a new hook useLanguage which will return the currently active language. I would recommend creating a new directory at src/hooks/ for the hooks. In that directory create a new file use-language.js and add the following code to it:

import { useStaticQuery, graphql } from 'gatsby';

export default function useLanguage() {
  const data = useStaticQuery(graphql`
    {
      site {
        siteMetadata {
          language
        }
      }
    }
  `);

  return data.site.siteMetadata.language;
}

Here we are defining a function or hook called useLanguage, which queries the language from GraphQL and returns it.  Not too hard, right?

Next up, we’ll create the hook useTranslation, this will return a function, which we can use to query the string for a given key. Create a new file called use-translation.js and add the following code to it:

import { useStaticQuery, graphql } from 'gatsby';
import useLanguage from './use-language';

export default function useTranslation() {
  const query = useStaticQuery(graphql`
    {
      allTranslation {
        nodes {
          key
          en
          de
        }
      }
    }
  `);
  let data = {};
  query.allTranslation.nodes.forEach(val => {
    const key = val.key;
    data[key] = {
      en: val.en,
      de: val.de,
    };
  });

  const lang = useLanguage();

  return key => {
    return data[key][lang];
  };
}

This hook queries all translations from GraphQL (the results are cached during the build process by Gatsby). It then loops over them, to convert them to a dictionary. Finally, it uses our other hook useLanguage to get the active language and returns a function which takes a key and returns the corresponding string.

Optional: Language Switcher

We can’t make proper use of translations, without allowing the user to switch between multiple languages. Let’s write a quick component which solves this problem:

// I named this file `language-switch.js` and put it in my `src/components` directory
import React from 'react';
import useLanguage from '../hooks/use-language';

const LanguageSwitcher = ({ ...props }) => {
  const lng = useLanguage() == 'en' ? 'de' : 'en';

  return (
    <a
      href={
        lng == 'en'
          ? 'https://yourdeversionofthewebsite.com'
          : 'https://yourenversionofthewebsite.com'
      }
      {...props}
    >
      View in {lng == 'en' ? '🇬🇧' : '🇩🇪'}
    </a>
  );
};

export default LanguageSwitcher;

Here, we use our useLanguage hook to get the active language and return a link pointing to the other version of the website. (Obviously, this would need to be solved differently, if we used this approach with multiple languages)

The Implementation

Now with all hooks and components created, it should be fairly straight forward to implement this: At the top of every component that currently contains strings that should be translated, use the useTranslation hook:

const t = useTranslation();

Now go through the component: Replace each of those strings with t("my-very-descriptive-key") and add the corresponding entry to your translations.json file.

After you have done this, you can add the LanguageSwitcher component to your navigation bar, or somewhere else, where it is easily visible.

Opportunities to expand this implementation

There are a few problems with this implementation you might encounter as the content of the site grows, but they should be fairly to address:

The user doesn’t automatically land on the correctly translated site

With other solutions, all paths get prefixed with a language code. In this solution, the domain gets prefixed with a language code, or it uses a country specific TLD. To make sure the user lands on the correct page, we could check which region the user’s browser reports and redirect them if necessary.

The translations file gets crowded very quickly

After at least 50 strings, you will find yourself using ctrl+f to find the string you want to edit, obviously this isn’t very scalable. gatsby-transformer-json, should by default take all JSON nodes and integrate them into one GraphQL query, but I haven’t tested if this works out of the box. If it doesn’t there should only be a few changes necessary to use-translation.js. Then we could split up the strings into one file per component or page.

Before you leave:

As you've come this far, you seem to dig my posts. Subscribe to my newsletter to receive a new one each week.

If you are enjoying my posts, it would really make my day if you supported me on ☕️ BuyMeACoffee by becoming a member or buying me a one-time chai tea. It only takes one reader becoming a member for $5/month to be able to integrate comments that respect your privacy into this blog and to spend more time creating content like this.

← back to all posts
Bruce Röttgers