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.
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:
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.
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.
Cosmic JS, a great headless CMS option, allows you do activate localization when you create a new object type.
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.
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
.
{
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.
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.
// 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.
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:
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! 😉
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.