smakosh
Posted on October 8, 2018
This article was originally published on Obytes blog.
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
gatsby 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
yarn add react-intl recompose styled-components@next 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
.
+-- src
+-- components
| |
| +-- common
| | +-- Head
| | | |
| | | +-- index.jsx
| | +-- Container
| | | |
| | | +-- index.jsx
| | +-- Context
| | | |
| | | +-- index.jsx
| | +-- Layout
| | | |
| | | +-- index.jsx
| | | +-- Provider.jsx
| | | +-- layout.css
| | +-- Trigger
| | | |
| | | +-- index.jsx
| | +-- index.js
| +-- theme
| | +-- Header
| | | |
| | | +-- index.jsx
+-- messages
| |
| +-- ar.json
| +-- en.json
+-- pages
|
+-- index.js
+-- 404.js
+-- about.js
Let's start by creating context inside Context component and have en
as the default value.
import React from 'react'
export 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.
import React from 'react'
import { compose, withState, withHandlers, lifecycle } from 'recompose'
import { Context } from '../Context'
const Provider = ({ children, lang, toggleLanguage }) => (
<Context.Provider value={
{ lang, toggleLanguage: () => toggleLanguage() }
}>
{children}
</Context.Provider>
)
const enhance = compose(
withState('lang', 'handleLanguage', 'en'),
withHandlers({
toggleLanguage: ({ lang, handleLanguage }) => () => {
if (lang === 'ar') {
handleLanguage('en')
localStorage.setItem('lang', 'en')
} else {
handleLanguage('ar')
localStorage.setItem('lang', 'ar')
}
}
}),
lifecycle({
componentDidMount() {
const localLang = localStorage.getItem('lang')
if (localLang) {
this.props.handleLanguage(localLang)
} else {
this.props.handleLanguage(navigator.language.split('-')[0])
}
}
})
)
export 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 usingreact-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.
import React from 'react'
import styled from 'styled-components'
import ar from 'react-intl/locale-data/ar'
import en from 'react-intl/locale-data/en'
import { addLocaleData, IntlProvider } from 'react-intl'
import localEng from '../../../messages/en.json'
import localAr from '../../../messages/ar.json'
import { Context } from '../Context'
import Provider from './Provider'
import Header from '../../theme/Header'
import './layout.css'
addLocaleData(ar, en)
const Layout = ({ children }) => (
<Provider>
<Context.Consumer>
{({ lang }) => (
<IntlProvider locale={lang} messages={lang === 'en' ? localEng : localAr}>
<Global lang={lang}>
<Header />
{children}
</Global>
</IntlProvider>
)}
</Context.Consumer>
</Provider>
)
const Global = styled.div`
font-family: 'Roboto', sans-serif;
${({ lang }) => lang === 'ar' && `
font-family: 'Cairo', sans-serif;
`}
`
export { 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
import React from 'react'
import styled from 'styled-components'
import { Link } from 'gatsby'
import { FormattedMessage } from 'react-intl'
import { Trigger, Container } from '../../common'
const Header = () => (
<StyledHeader>
<Navbar as={Container}>
<Link to="/">
<FormattedMessage id="logo_text" />
</Link>
<Links>
<Link to="/">
<FormattedMessage id="home" />
</Link>
<Link to="/about">
<FormattedMessage id="about" />
</Link>
<Trigger />
</Links>
</Navbar>
</StyledHeader>
)
// Feel free to move these to a separated styles.js file and import them above
const StyledHeader = styled.div`
padding: 1rem 0;
background: #00BCD4;
`
const Navbar = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
a {
color: #fff;
text-decoration: none;
}
`
const Links = styled.div`
display: flex;
align-items: center;
a {
margin: 0 1rem;
}
`
export default Header
We separated the button that changes the language alone so we can understand it well
import React from 'react'
import styled from 'styled-components'
import { FormattedMessage } from 'react-intl'
import { Context } from '../Context'
const Trigger = () => (
<Context.Consumer>
{({ toggleLanguage }) => (
<Button type="button" onClick={toggleLanguage}>
<FormattedMessage id="language" />
</Button>
)}
</Context.Consumer>
)
// We recommend moving the style down below to a separate file
const Button = styled.button`
color: #fff;
padding: .3rem 1rem;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
background: #3F51B5;
border-radius: 4px;
font-size: 15px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .025em;
text-decoration: none;
cursor: pointer;
&:focus {
outline: none;
}
`
export { 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
.
import React from 'react'
import { Helmet } from 'react-helmet'
import { injectIntl } from 'react-intl'
import { Context } from '../Context'
const Head = ({ title, intl: { formatMessage } }) => (
<Context.Consumer>
{({ lang }) => (
<Helmet>
<html lang={lang} dir={lang === 'ar' ? 'rtl' : 'ltr'} />
<title>
${formatMessage({ id: title })}
</title>
</Helmet>
)}
</Context.Consumer>
)
export 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.
module.exports = {
siteMetadata: {
title: 'Gatsby i18n Example',
},
plugins: [
'gatsby-plugin-react-helmet',
'gatsby-plugin-styled-components',
{
resolve: 'gatsby-plugin-google-fonts',
options: {
fonts: [
'Cairo',
'Roboto'
]
}
},
{
resolve: 'gatsby-plugin-manifest',
options: {
name: 'gatsby-starter-default',
short_name: 'starter',
start_url: '/',
background_color: '#663399',
theme_color: '#663399',
display: 'minimal-ui',
icon: 'src/images/gatsby-icon.png',
},
},
'gatsby-plugin-offline',
],
}
- Home page
import React from 'react'
import { FormattedMessage } from 'react-intl'
import { Layout, Container } from '../components/common'
import Head from '../components/common/Head'
const IndexPage = () => (
<Layout>
<>
<Head title="welcome" />
<Container>
<h2>
<FormattedMessage id="welcome" />
</h2>
</Container>
</>
</Layout>
)
export default IndexPage
- About page
import React from 'react'
import { FormattedMessage } from 'react-intl'
import { Layout, Container } from '../components/common'
import Head from '../components/common/Head'
const AboutPage = () => (
<Layout>
<>
<Head title="about" />
<Container>
<h2>
<FormattedMessage id="about" />
</h2>
</Container>
</>
</Layout>
)
export default AboutPage
And here is both the json files that contain the messages we're using in this example:
{
"language": "عربي",
"welcome": "Welcome",
"Logo_text": "Logo",
"Home": "Home",
"About": "About",
"not_found": "404 - Page Not Found"
}
{
"language": "English",
"welcome": "أهلا بك",
"Logo_text": "شعار",
"Home": "الرئيسية",
"About": "معلومات عنا",
"not_found": "الصفحة غير موجودة - 404"
}
Let's test this out by running
yarn 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.
Posted on October 8, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.