Websites are global by nature, but it is becoming increasingly relevant for businesses of all types to create a multilingual website. To maximise sales and production, businesses must make online presence easily accessible to targeted customers. The safest bet would be a single language website, preferably in English. However, English is commonly regarded as the most used world language but 70% of web surfers don’t speak English as their first language.
Now we’ve determined multilingual support is important, so let’s jump to the problem.
A recent project I’ve been building is a static website written with a popular new framework for building – well, static websites. It is called GatsbyJs and seriously – go check it out if you didn’t already, it is awesome!
The problem I faced was to use Croatian as the main (default) language, and English as the second language. I had no idea how to do that with GatsbyJs since it was my first project with it. So what did I do? I googled. A lot.
After several hours of research, reading Stack Overflow and Github issues, I chose the approach to go with.
The solution
Since GatsbyJs is written with React, same libraries can be used. I went for React-intl, a React library that provides an API and allows binding components directly. Basically, you create one component and use it for every text instance on website. Component takes care of selected language and it provides a corresponding translation.
Since GatsbyJs is a static site generator, we need to achieve static rendering, in other words, we need to duplicate each page for each locale (language).
The first thing to do is to store locale definition somewhere in the project, I stored it in subfolder constants/locales.js :
module.exports={
hr: {
path: 'hr',
locale: 'HR',
default: true
},
en: {
path: 'en',
locale: 'EN',
}
}
Now, since the pages need to render statically, we need to create page duplicates for each locale. I used node API which is part of GatsbyJs. Using Node, every page needs to be deleted and replaced with our version, which is done in the gatsby-node.js file in the root folder :
const locales = require('./src/constants/locales')
const path = require('path')
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions
return new Promise(resolve => {
deletePage(page)
Object.keys(locales).map(lang => {
const localizedPath = locales[lang].default
? page.path
: locales[lang].path + page.path
return createPage({
...page,
path: localizedPath,
context: {
locale: lang
}
})
})
resolve()
})
}
Basically, all we do is deleting the page, and then creating it again for each locale but this time passing the locale to Page context to use it later.
Now react-intl can be used finally.
As stated previously each page receives locale in PageContext object. So the idea for every page is to use that object and pass it to Layout component which is used to define website skeleton in GatsbyJs.
Before we see how that looks like, we need to create JSON files which will hold our strings to translate. I stored them in i18n subfolder.
src/i18n/en.json :
{
"servicesPage": "Services",
"aboutPage": "About",
"recommendPage": "Recommend us",
"blogPage": "News",
"faqPage": "FAQ",
"contactPage": "Contact us",
"meetingPage": "Arrange meeting",
"headerInfoText": "Have a question?You can contact us 24/7! ",
}
src/i18n/hr.json :
{
"servicesPage": "Usluge",
"aboutPage": "O nama",
"recommendPage": "Preporuči nas",
"blogPage": "Novosti",
"faqPage": "Česta pitanja",
"contactPage": "Kontakt",
"meetingPage": "Dogovori sastanak",
"headerInfoText": "Imaš pitanja? Dostupni smo 24/7!"
}
After we defined our translation, the last thing to do is configure our Layout component which looks like this:
import React from 'react'
import styled, { css } from 'styled-components'
import './layout.scss'
import { IntlProvider, addLocaleData, FormattedMessage } from 'react-intl'
import hrData from 'react-intl/locale-data/hr'
import enData from 'react-intl/locale-data/en'
import en from '../i18n/en.json'
import hr from '../i18n/hr.json'
const messages = { hr, en }
addLocaleData([...hrData, ...enData])
class Layout extends React.Component {
state = {
isOpen: false,
isMounted: false,
}
toggleHamburger = () => {
window.innerWidth < 960 && this.setState({ isOpen: !this.state.isOpen })
}
render() {
const {
locale,
} = this.props
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<>
Let’s break it up a bit.
First, we are importing three things from the react-intl library:
- IntlProvider – makes sure locale is passed to children so we can use translations
- addLocaleData – function which defines locales imported from library itself
- FormattedMessage – a component which takes care of our translations
So, basically Layout is being used on every page, and its children can access locale thanks to IntlProvider mentioned above. The provider also takes messages as a prop, so we can also access every expression we want to be translated.
We can now use the Layout component on our pages and it looks something like this:
const IndexPage = ({ pageContext: { locale }, }) => {
return (
<Layout
fluid={data.file.childImageSharp.fluid}
isHomePage={isHomePage}
locale={locale}
>
<CardList locale={locale}/>
<MobileScreens locale={locale}/>
</Layout>
)
}
The last thing to mention is the Gatsby Link component. Since Gatsby has its own component for linking pages which takes route name as the source of truth. So for example, we have route ‘/o-nama’, and we switch to the english version, the link will still take us to the same page, which is wrong. We wanted to go to the page ‘/en/about-us’.
One solution would be to create some component which will extend the Link component:
import React from 'react'
import { Link } from 'gatsby'
import { injectIntl} from 'react-intl'
import locales from '../constants/locales'
const ExtendedLink = ({ to, intl: { locale }, ...props }) => {
const path = locales[locale].default ? to : `/${locale}${to}`
return <Link to={path} />
}
export default injectIntl(ExtendedLink)
React-intl provides injectIntl HOC which provides the object with locale property. We can destructure it and use it to define the path to our page, then pass that path to Link component. And that’s it, we can now use ExtendedLink component instead of Link and be sure it will take us to the desired path based on provided locale.
The Conclusion
GatsbyJs doesn’t come with i18n support out-of-the-box, but with the approach given above you can still manage to do it easily. Integrated Node API allows us to maintain static rendering by duplicating pages for each locale. Integrating react-intl library and using it in our project was fairly easy, and from there you can adjust behaviour based on the project requirements.
You can add as many languages as you want. You just need to define them in constants and point to them in the Layout component. The rest of the code stays the same.
Considering Gatsby’s tempo and the way its community is growing, I’m sure someone will develop the package which will take care of all of the stuff mentioned above, but until then, the approach given above can satisfy most of the requests for multi-language support.
UPDATE (Nov 2019): Since gatsby-plugin-intl is now available it should satisfy most of the needs for handling multilanguage support in Gatsby (credits to Michal Honc for pointing at that in the comment)