Creating a Multilingual Website with Gatsby & Cosmic JS

Jack Misteli

A friend recently asked me to build a website which has a Japanese and an English version. I never wrote an internationalized website but I have strong opinions about how localization should work. So here is one way to approach internationalization in Gatsby using Cosmic JS, a headless CMS.

A Bit of Context

There are many ways to approach localization and as always there is no silver bullet. Each approach solves the problem in different ways. So here is my context:

  • I want to build a site with as little maintenance cost as possible.
  • The content writer doesn’t have any programing experience
  • The site’s maintenance costs should be as low as possible.
  • The site should not force the user on one version of the site.

This last point is so important to me. When you are in a foreign country, some websites force you to use the local version of their site. When [array of multinational companies] forces me to the [long array of languages I don't understand] version of the website it drives me mad. I have the same issue with automatic translations of a page. If I want an automatic translation of my website, I can use the fantastic Google translate Chrome extension.

This website is for both Japanese and English users. So all the pages of the site should have an English version and a Japanese version. If the user wants to change the current version of the website she can click a language menu in the navigation bar.

↓ Here's a great React course we recommend. Plus, this affiliate banner helps support the site 🙏

My approach

Gatsby and React offer many tools to approach localization (l10n) and internationalization (i18n).

I first used gatsby-plugin-i18n to easily generate routes.

For example, /page/team.ja.js will generate the following URL: /ja/team (ja is the language code for Japan).

This is a really nice plugin but the problem is that it isn’t programmatic. I have to write a new file for each language. In each file, I have to make a specific GraphQL query to fetch the data. So for example, if I introduce a new language to my CMS I have to create all the routes again with the new language extension.

So instead, I decided to build l10n without any plugin. All the code for this project is available at https://github.com/alligatorio/kodou.

In this context, the content writer is fully responsible for localization. When she writes the Japanese version of the website, she should make sure that the date formats are correct. This is why we are not using react-intl which relies on the Internationalization API and will be the topic of a future post.

Setting up Cosmic JS

Cosmic JS, a great headless CMS option, allows you do activate localization when you create a new object type.

screenshot: l10n option in Cosmic JS

Don’t forget to select a priority locale otherwise the new object won’t save.

In our new site we have a team page so we create a Team Members object. When we create a new Team Member we now can choose its’ language.

screenshot: Team Member language selection in Cosmic JS

Now to access that data from Gatsby we need to add the gatsby-source-cosmicjs source plugin:

$ yarn add gatsby-source-cosmicjs

Then we need to configure gatsby-config.js to use gatsby-source-cosmicjs by adding the following code in plugins.

Module: gatsby-config.js

{
  resolve: "gatsby-source-cosmicjs",
  options: {
    bucketSlug: process.env.COSMIC_BUCKET,
    // We add the 'team-members' object type to be able to fetch it later
    objectTypes: ["team-members"],
    // If you have enabled read_key to fetch data (optional).
    apiAccess: {
      read_key: process.env.COSMIC_ENV_KEY,
    }
  }
}

In the rest of our code we can access the team member data from Cosmic JS by running:

graphql(`
  {
    allCosmicjsTeamMembers  {
      edges {
      # Here we have the structure of out `team-members` object
        node {
          title
          locale
          content
          metadata {
            profile_picture {
              imgix_url
            }
          }
        }
      }
    }
  }
`)

Now the localization magic happens.

Generating Localized Pages

I wanted my friend to be able to do any changes he wanted by himself. So I completely dropped the /pages directory in favor of the /templates directory. Gatsby templates allow us to have reusable content and programmatically create pages; which is exactly what we need to do!

Before we look at our template file let’s see how we can fetch data from Cosmic JS to create new pages.

Module: gatsby-node.js

// langs contains the languages of our blog and default langKey is the default language of the site
// To be fully programmatic we could calculate langs
// here langs = ['en', 'ja'] and defaultLangKey = 'en'
const { langs, defaultLangKey } = require('../config/languages')
const path = require(`path`)
const { localizeUrl, createLanguagesObject } = require('../utils/localization')

exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions

  const result = await graphql(`
    {
      allCosmicjsTeamMembers  {
        edges {
          node {
            title
            locale
            content
            metadata {
              profile_picture {
                imgix_url
              }
            }
          }
        }
      }
    }
  `)

  if (result.errors) {
    console.error(result.errors)
  }

  // Creates a profiles object with out site's languages 
  const profiles = createLanguagesObject(langs)
  // profiles = {
  // 'en': [],
  // 'ja': []  
  // }

  // converting the raw cosmic data into a more useable data structure
  result.data.allCosmicjsTeamMembers.edges.forEach(({ node }) => {
    profiles[node.locale].push(node)
  })
  // profiles = {
  // 'en': [...all English profiles],
  // 'ja': [...all Japanese profiles]  
  // }

  // we create a new page for each language
  langs.forEach(lang =>{
     createPage({
      // the localizeUrl function creates a url which takes into consideration what the default language is
      path: localizeUrl(lang, defaultLangKey, '/team'),
      component: path.resolve(`src/templates/team.js`),
      context: {
        profiles: profiles[lang]
      }
    })
  })
}

This code will create two new pages with the paths /ja/team and /team (There is no /en since we set English as the default language).

As you can see the createPage takes as an argument an object with 3 fields path, component and context. Path is simply the path we want our new page to have. component is the template we want to use. context is the data we want to pass to our template. Here we pass the profiles written in our desired language.

Templating

Let’s take a look at our team template.

import React from "react"
import Layout from "../components/layout"
import SEO from "../components/seo"

const TeamPage = (props) => {
  // We will see about pageContext in the next section
  const {profiles} = props.pageContext
  return (
  <Layout location={props.location}>
    <SEO title="Team" />
    <h1>Team</h1>
    // Iterating trough the array of profiles
    {profiles.map((profile,i)=>(
      <div key={i} className="columns">
        <div className="column">
          // Here are some nice profile pictures of our team members
          <div className="square-image" style={{backgroundImage: `url("${profile.metadata.profile_picture.imgix_url}")`}}/>
        </div>
        <div className="column is-two-thirds">
          <div className="team-member-title">{profile.title}</div>
          // Here is some html content we get from Cosmic
          <div dangerouslySetInnerHTML={{ __html: profile.content }}/>
        </div>
      </div>
      )
    )}
  </Layout>
  )
}

export default TeamPage

To sum up, the code above takes a profiles props which is an array of profiles we get from Cosmic JS. Each profile has a profile picture object, a title and a content field. The content is actually a string of HTML so we have to set it using the dangerouslySetInnerHTML prop.

For this template to work, it’s important to prepare your CSS files in advance to get consistent results. My friend won’t be able to add class names or ids in Cosmic’s WYSIWYG.

There is much more to say and do:

  • Creating a navbar and location aware Layout
  • How to use the internationalization API
  • How to softly redirect users to their version of the site

You can explore the Github repo to find out how I address these issues and see the results on kodou.me. Or use Alligator.io to see if we uploaded some new content on that topic. But I think it’s already a lot to process in one post. Above I hope this will help a little or a lot to build your own internationalized site and stay tuned for more to come! 😉

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#