Implement i18n to a Gatsby site.

Ismail Ghallou
October 7th, 2018 · 2 min read

In this article we will implement i18n (Internationalization) to a Gatsby site using react-intl and React context API, we will only cover English and Arabic in this article but you could add more languages if you wish to, before we get started, let’s first of all plan how we want to implement it.

1- Detect the user’s default language

2- Automatically switch the language, direction of content and the font family depending on the user’s default language

3- The user still can choose their preferred language

Let’s start by generating a new Gatsby site using their CLI tool

1gatsby new gatsby-i18n-example && cd gatsby-i18n-example/

Then we will install the libraries we need (I’m using 'yarn' but feel free to use 'npm')

I’m installing recompose too to separate logic from the component and keep the code clean (Feel free not to use it but We highly recommend it), as well as styled-components beta v4 to handle css in js (Feel free not use it too but We highly recommend it) and a simple google fonts gatsby plugin 'gatsby-plugin-google-fonts'

1yarn add react-intl recompose [email protected] babel-plugin-styled-components gatsby-plugin-styled-components gatsby-plugin-google-fonts

Before we start, let’s first structure the files in a better way like down below

1.
2+-- src
3 +-- components
4 | |
5 | +-- common
6 | | +-- Head
7 | | | |
8 | | | +-- index.jsx
9 | | +-- Container
10 | | | |
11 | | | +-- index.jsx
12 | | +-- Context
13 | | | |
14 | | | +-- index.jsx
15 | | +-- Layout
16 | | | |
17 | | | +-- index.jsx
18 | | | +-- Provider.jsx
19 | | | +-- layout.css
20 | | +-- Trigger
21 | | | |
22 | | | +-- index.jsx
23 | | +-- index.js
24 | +-- theme
25 | | +-- Header
26 | | | |
27 | | | +-- index.jsx
28 +-- messages
29 | |
30 | +-- ar.json
31 | +-- en.json
32 +-- pages
33 |
34 +-- index.js
35 +-- 404.js
36 +-- about.js

Let’s start by creating context inside Context component and have 'en' as the default value.

1import React from 'react'
2
3export const Context = React.createContext('en')

Now let’s get to the Provider component that passes the global state to the Consumers that are descendants of it.

Provider is a React component that allows Consumers to subscribe to context changes.

1import React from 'react'
2import { compose, withState, withHandlers, lifecycle } from 'recompose'
3import { Context } from '../Context'
4
5const Provider = ({ children, lang, toggleLanguage }) => (
6 <Context.Provider value={{ lang, toggleLanguage: () => toggleLanguage() }}>
7 {children}
8 </Context.Provider>
9)
10
11const enhance = compose(
12 withState('lang', 'handleLanguage', 'en'),
13 withHandlers({
14 toggleLanguage: ({ lang, handleLanguage }) => () => {
15 if (lang === 'ar') {
16 handleLanguage('en')
17 localStorage.setItem('lang', 'en')
18 } else {
19 handleLanguage('ar')
20 localStorage.setItem('lang', 'ar')
21 }
22 },
23 }),
24 lifecycle({
25 componentDidMount() {
26 const localLang = localStorage.getItem('lang')
27 if (localLang) {
28 this.props.handleLanguage(localLang)
29 } else {
30 this.props.handleLanguage(navigator.language.split('-')[0])
31 }
32 },
33 })
34)
35
36export default enhance(Provider)

This will wrap all our components so that we can access the value which contains 'lang' and a function to toggle the language called 'toggleLanguage' and below the component is the logic.

We initialized 'lang' with a default value of 'en', but that can change when the component mounts, we check if localStorage is available, if true: we assign its value to lang state, else: we detect the user’s browser’s default language and split the value to get the first item that contains the language.

Now move on to the 'Layout' component where:

  • we will import both english and arabic json data
  • along with the 'IntlProvider' to wrap the content where we will be using 'react-intl' built in components
  • as well as importing 'Context' and wrap our content with its Consumer so we can access the global state
  • finally wrapping everything by 'Provider' we created above.
1import React from 'react'
2import styled from 'styled-components'
3import ar from 'react-intl/locale-data/ar'
4import en from 'react-intl/locale-data/en'
5import { addLocaleData, IntlProvider } from 'react-intl'
6import localEng from '../../../messages/en.json'
7import localAr from '../../../messages/ar.json'
8import { Context } from '../Context'
9import Provider from './Provider'
10import Header from '../../theme/Header'
11import './layout.css'
12
13addLocaleData(ar, en)
14
15const Layout = ({ children }) => (
16 <Provider>
17 <Context.Consumer>
18 {({ lang }) => (
19 <IntlProvider
20 locale={lang}
21 messages={lang === 'en' ? localEng : localAr}
22 >
23 <Global lang={lang}>
24 <Header />
25 {children}
26 </Global>
27 </IntlProvider>
28 )}
29 </Context.Consumer>
30 </Provider>
31)
32
33const Global = styled.div`
34 font-family: 'Roboto', sans-serif;
35 ${({ lang }) =>
36 lang === 'ar' &&
37 `
38 font-family: 'Cairo', sans-serif;
39 `}
40`
41
42export { Layout }

We forgot to mention that we used the 'Global' component just to handle the font change, so it will be 'Roboto' when the language is set to english and 'Cairo' when it is set to arabic.

Now that everything to make it work is ready, let’s add a button to the header to toggle the language

1import React from 'react'
2import styled from 'styled-components'
3import { Link } from 'gatsby'
4import { FormattedMessage } from 'react-intl'
5import { Trigger, Container } from '../../common'
6
7const Header = () => (
8 <StyledHeader>
9 <Navbar as={Container}>
10 <Link to="/">
11 <FormattedMessage id="logo_text" />
12 </Link>
13 <Links>
14 <Link to="/">
15 <FormattedMessage id="home" />
16 </Link>
17 <Link to="/about">
18 <FormattedMessage id="about" />
19 </Link>
20 <Trigger />
21 </Links>
22 </Navbar>
23 </StyledHeader>
24)
25
26// Feel free to move these to a separated styles.js file and import them above
27
28const StyledHeader = styled.div`
29 padding: 1rem 0;
30 background: #00bcd4;
31`
32const Navbar = styled.div`
33 display: flex;
34 align-items: center;
35 justify-content: space-between;
36 a {
37 color: #fff;
38 text-decoration: none;
39 }
40`
41const Links = styled.div`
42 display: flex;
43 align-items: center;
44 a {
45 margin: 0 1rem;
46 }
47`
48
49export default Header

We separated the button that changes the language alone so we can understand it well

1import React from 'react'
2import styled from 'styled-components'
3import { FormattedMessage } from 'react-intl'
4import { Context } from '../Context'
5
6const Trigger = () => (
7 <Context.Consumer>
8 {({ toggleLanguage }) => (
9 <Button type="button" onClick={toggleLanguage}>
10 <FormattedMessage id="language" />
11 </Button>
12 )}
13 </Context.Consumer>
14)
15
16// We recommend moving the style down below to a separate file
17
18const Button = styled.button`
19 color: #fff;
20 padding: 0.3rem 1rem;
21 box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
22 background: #3f51b5;
23 border-radius: 4px;
24 font-size: 15px;
25 font-weight: 600;
26 text-transform: uppercase;
27 letter-spacing: 0.025em;
28 text-decoration: none;
29 cursor: pointer;
30 &:focus {
31 outline: none;
32 }
33`
34
35export { Trigger }

We imported 'Context' once again in this file so we can use its 'Consumer' so we get the global state. Now when the button is clicked, the'toggleLanguage' function changes the 'lang' value.

Before we get the Gatsby config file, let’s take care of the direction of content as well by accessing the 'lang' value from the consumer of context and conditionally check if it’s arabic, if true the direction must become 'rtl', else 'lrt'.

1import React from 'react'
2import { Helmet } from 'react-helmet'
3import { injectIntl } from 'react-intl'
4import { Context } from '../Context'
5
6const Head = ({ title, intl: { formatMessage } }) => (
7 <Context.Consumer>
8 {({ lang }) => (
9 <Helmet>
10 <html lang={lang} dir={lang === 'ar' ? 'rtl' : 'ltr'} />
11 <title>${formatMessage({ id: title })}</title>
12 </Helmet>
13 )}
14 </Context.Consumer>
15)
16
17export default injectIntl(Head)

You could include all the meta tags, opengraph, structured data and Twitter tags in this 'Head' component like I did in the gatsby-i18n-starter

Finally let’s include the plugins we’re using into the 'gatsby-config.js' file and let’s prepare some dummy pages with some messages that support i18n.

1module.exports = {
2 siteMetadata: {
3 title: 'Gatsby i18n Example',
4 },
5 plugins: [
6 'gatsby-plugin-react-helmet',
7 'gatsby-plugin-styled-components',
8 {
9 resolve: 'gatsby-plugin-google-fonts',
10 options: {
11 fonts: ['Cairo', 'Roboto'],
12 },
13 },
14 {
15 resolve: 'gatsby-plugin-manifest',
16 options: {
17 name: 'gatsby-starter-default',
18 short_name: 'starter',
19 start_url: '/',
20 background_color: '#663399',
21 theme_color: '#663399',
22 display: 'minimal-ui',
23 icon: 'src/images/gatsby-icon.png',
24 },
25 },
26 'gatsby-plugin-offline',
27 ],
28}
  • Home page
1import React from 'react'
2import { FormattedMessage } from 'react-intl'
3import { Layout, Container } from '../components/common'
4import Head from '../components/common/Head'
5
6const IndexPage = () => (
7 <Layout>
8 <>
9 <Head title="welcome" />
10 <Container>
11 <h2>
12 <FormattedMessage id="welcome" />
13 </h2>
14 </Container>
15 </>
16 </Layout>
17)
18
19export default IndexPage
  • About page
1import React from 'react'
2import { FormattedMessage } from 'react-intl'
3import { Layout, Container } from '../components/common'
4import Head from '../components/common/Head'
5
6const AboutPage = () => (
7 <Layout>
8 <>
9 <Head title="about" />
10 <Container>
11 <h2>
12 <FormattedMessage id="about" />
13 </h2>
14 </Container>
15 </>
16 </Layout>
17)
18
19export default AboutPage

And here is both the json files that contain the messages we’re using in this example:

1{
2 "language": "عربي",
3 "welcome": "Welcome",
4 "Logo_text": "Logo",
5 "Home": "Home",
6 "About": "About",
7 "not_found": "404 - Page Not Found"
8}
1{
2 "language": "English",
3 "welcome": "أهلا بك",
4 "Logo_text": "شعار",
5 "Home": "الرئيسية",
6 "About": "معلومات عنا",
7 "not_found": "الصفحة غير موجودة - 404"
8}

Let’s test this out by running

1yarn develop

It seems to work 🎉, check the demo, here’s the link to the repository in case you couldn’t follow up, have a question? leave it on the comments and we will answer it ASAP.

Feel free to use my own gatsby-i18n-starter to easily get started with some other great features.

More articles from Obytes

Python web scraping (part 1).

Python web scraping basics, definitions, general aspects, main tools and challenges

September 23rd, 2018 · 4 min read

Deploy Wasabi using a remote Cassandra cluster and RDS.

Wasabi is a real-time, 100% API driven, A/B Testing platform. We will see how to deploy it on AWS along with Cassandra and a MySQL RDS.

September 15th, 2018 · 3 min read

ABOUT US

Our mission and ambition is to challenge the status quo, by doing things differently we nurture our love for craft and technology allowing us to create the unexpected.