Gatsby Headaches And How To Cure Them: i18n (Part 1)
Internationalization, or i18n, is making your content understandable in other languages, regions, and cultures to reach a wider array of people. However, a more interesting question would be, “Why is i18n important?”. The answer is that we live in an era where hundreds of cultures interact with each other every day, i.e., we live in a globalized world. However, our current internet doesn’t satisfy its globalized needs.
Did you know that 60.4% of the internet is in English, but only 16.2% percent of the world speaks English?
Source: Visual Capitalist
Yes, it’s an enormous gap, and until perfect AI translators are created, the internet community must close it.
As developers, we must adapt our sites’ to support translations and formats for other countries, languages, and dialects, i.e., localize our pages. There are two main problems when implementing i18n on our sites.
- Storing and retrieving content.
We will need files to store all our translations while not bloating our page’s bundle size and a way to retrieve and display the correct translation on each page. - Routing content.
Users must be redirected to a localized route with their desired language, likemy-site.com/es
oren.my-site.com
. How are we going to create pages for each locale?
Fortunately, in the case of Gatsby and other static site generators, translations don’t bloat up the page bundle size since they are delivered as part of the static page. The rest of the problems are widely known, and there are a lot of plugins and libraries available to address them, but it can be difficult to choose one if you don’t know their purpose, what they can do, and if they are compatible with your existing codebase. That’s why in the following hands-on guide, we will see how to use several i18n plugins for Gatsby and review some others.
The Starter
Before showing what each plugin can do and how to use them, we first have to start with a base example. (You can skip this and download the starter here). For this tutorial, we will work with a site with multiple pages created from an array of data, like a blog or wiki. In my case, I choose a cooking blog that will initially have support only for English.
Start A New Project
To get started, let’s start a plain JavaScript Gatsby project without any plugins at first.
npm init gatsby
cd my-new-site
For this project, we will create pages dynamically from markdown files. To be able to read and parse them to Gatsby’s data layer, we will need to use the gatsby-source-filesystem
and gatsby-transformer-remark
plugins. Here you can see a more in-depth tutorial.
npm i gatsby-source-filesystem gatsby-transformer-remark
Inside our gatsby-config.js
file, we will add and configure our plugins to read all the files in a specified directory.
// ./gatsby-config.js
module.exports = {
//...
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `content`,
path: `${__dirname}/src/content`,
},
},
`gatsby-transformer-remark`,
],
};
Add Your Content
As you can see, we will use a new ./src/content/
directory where we will save our posts. We will create a couple of folders with each recipe’s content in markdown files, like the following:
├── src
│ ├── content
| | ├── mac-and-cheese
| | | ├── cover.jpg
| | | ├── index.en.md
| | ├── burritos
| | | ├── cover.jpg
| | | ├── index.en.md
| | ├── pizza
| | | ├── cover.jpg
| | | ├── index.en.md
│ ├── pages
│ ├── images
Each markdown file will have the following structure:
---
slug: "mac-and-cheese"
date: "2023-01-20"
title: "How to make mac and cheese"
cover_image:
image: "./cover.jpg"
alt: "Macaroni and cheese"
locale: "en"
---
Step 1
Lorem ipsum...
You can see that the first part of the markdown file has a distinct structure and is surrounded by ---
on both ends. This is called the frontmatter and is used to save the file’s metadata. In this case, the post’s title, date, locale, etc.
As you can see, we will be using a cover.jpg
file for each post, so to parse and use the images, we will need to install the gatsby-plugin-image
gatsby-plugin-sharp
and gatsby-transformer-sharp
plugins (I know there are a lot 😅).
npm i gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp
We will also need to add them to the gatsby-config.js
file.
// ./gatsby-config.js
module.exports = {
//...
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `content`,
path: `${__dirname}/src/content`,
},
},
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
`gatsby-transformer-remark`,
`gatsby-plugin-image`,
],
};
Querying Your Content
We can finally start our development server:
npm run develop
And go to http://localhost:8000/___graphql
, where we can make the following query:
query Query {
allMarkdownRemark {
nodes {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
}
}
}
And get the following result:
{
"data": {
"allMarkdownRemark": {
"nodes": [
{
"frontmatter": {
"slug": "/mac-and-cheese",
"title": "How to make mac and cheese",
"date": "2023-01-20",
"cover_image": {
/* ... */
}
}
},
{
"frontmatter": {
"slug": "/burritos",
"title": "How to make burritos",
"date": "2023-01-20",
"cover_image": {
/* ... */
}
}
},
{
"frontmatter": {
"slug": "/pizza",
"title": "How to make Pizza",
"date": "2023-01-20",
"cover_image": {
/* ... */
}
}
}
]
}
}
}
Now the data is accessible through Gatsby’s data layer, but to access it, we will need to run a query from the ./src/pages/index.js
page.
Go ahead and delete all the boilerplate on the index
page. Let’s add a short header for our blog and create the page query:
// src/pages/index.js
import * as React from "react";
import {graphql} from "gatsby";
const IndexPage = () => {
return (
<main>
<h1>Welcome to my English cooking blog!</h1>
<h2>Written by Juan Diego Rodríguez</h2>
</main>
);
};
export const indexQuery = graphql`
query IndexQuery {
allMarkdownRemark {
nodes {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
}
}
}
`;
export default IndexPage;
Displaying Your Content
The result from the query is injected into the IndexPage
component as a props
property called data
. From there, we can render all the recipes’ information.
// src/pages/index.js
// ...
import {RecipePreview} from "../components/RecipePreview";
const IndexPage = ({data}) => {
const recipes = data.allMarkdownRemark.nodes;
return (
<main>
<h1>Welcome to my English cooking blog!</h1>
<h2>Written by Juan Diego Rodríguez</h2>
{recipes.map(({frontmatter}) => {
return <RecipePreview key={frontmatter.slug} data={frontmatter} />;
})}
</main>
);
};
// ...
The RecipePreview
component will be the following in a new directory: ./src/components/
:
// ./src/components/RecipePreview.js
import * as React from "react";
import {Link} from "gatsby";
import {GatsbyImage, getImage} from "gatsby-plugin-image";
export const RecipePreview = ({data}) => {
const {cover_image, title, slug} = data;
const cover_image_data = getImage(cover_image.image.childImageSharp.gatsbyImageData);
return (
<Link to={`/recipes/${slug}`}>
<h1>{title}</h1>
<GatsbyImage image={cover_image_data} alt={cover_image.alt} />
</Link>
);
};
Creating Pages From Your Content
If we go to http://localhost:8000/
, we will see all our recipes listed, but now we have to create a custom page for each recipe. We can do it using Gatsby’s File System Route API. It works by writing a GraphQL query inside the page’s filename, generating a page for each query result. In this case, we will make a new directory ./src/pages/recipes/
and create a file called {markdownRemark.frontmatter__slug}.js
. This filename translates to the following query:
query MyQuery {
allMarkdownRemark {
nodes {
frontmatter {
slug
}
}
}
}
And it will create a page for each recipe using its slug
as the route.
Now we just have to create the post’s component to render all its data. First, we will use the following query:
query RecipeQuery {
markdownRemark {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
html
}
}
This will query the first markdown file available in our data layer, but to specify the markdown file needed for each page, we will need to use variables in our query. The File System Route API injects the slug
in the page’s context in a property called frontmatter__slug
. When a property is in the page’s context, it can be used as a query variable under a $
followed by the property name, so the slug will be available as $frontmatter__slug
.
query RecipeQuery {
query RecipeQuery($frontmatter__slug: String) {
markdownRemark(frontmatter: {slug: {eq: $frontmatter__slug}}) {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
html
}
}
}
The page’s component is pretty simple. We just get the query data from the component’s props
. Displaying the title and date is straightforward, and the html
can be injected into a p
tag. For the image, we just have to use the GatsbyImage
component exposed by the gatsby-plugin-image
.
// src/pages/recipes/{markdownRemark.frontmatter__slug}.js
const RecipePage = ({data}) => {
const {html, frontmatter} = data.markdownRemark;
const {title, cover_image, date} = frontmatter;
const cover_image_data = getImage(cover_image.image.childImageSharp.gatsbyImageData);
return (
<main>
<h1>{title}</h1>
<p>{date}</p>
<GatsbyImage image={cover_image_data} alt={cover_image.alt} />
<p dangerouslySetInnerHTML={{__html: html}}></p>
</main>
);
};
//...
The last thing is to use the Gatsby Head API to change the page’s title to the recipe’s title. This can be easily done since the query’s data
is also available in the Head
component.
// src/pages/recipes/{markdownRemark.frontmatter__slug}.js
//...
export const Head = ({data}) => <title>{data.markdownRemark.frontmatter.title}</title>;
Summing all up results in the following code:
// src/pages/recipes/{markdownRemark.frontmatter__slug}.js
import * as React from "react";
import {GatsbyImage, getImage} from "gatsby-plugin-image";
import {graphql} from "gatsby";
const RecipePage = ({data}) => {
const {html, frontmatter} = data.markdownRemark;
const {title, cover_image, date} = frontmatter;
const cover_image_data = getImage(cover_image.image.childImageSharp.gatsbyImageData);
return (
<main>
<h1>{title}</h1>
<p>{date}</p>
<GatsbyImage image={cover_image_data} alt={cover_image.alt} />
<p dangerouslySetInnerHTML={{__html: html}}></p>
</main>
);
};
export const recipeQuery = graphql`
query RecipeQuery($frontmatter__slug: String) {
markdownRemark(frontmatter: {slug: {eq: $frontmatter__slug}}) {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
html
}
}
`;
export default RecipePage;
export const Head = ({data}) => <title>{data.markdownRemark.frontmatter.title}</title>;
Creating Localized Content
With all this finished, we have a functioning recipe blog in English. Now we will use each plugin to add i18n features and localize the site (for this tutorial) for Spanish speakers. But first, we will make a Spanish version for each markdown file in ./src/content/
. Leaving a structure like the following:
├── src
│ ├── content
| | ├── mac-and-cheese
| | | ├── cover.jpg
| | | ├── index.en.md
| | | ├── index.es.md
| | ├── burritos
| | | ├── cover.jpg
| | | ├── index.en.md
| | | ├── index.es.md
| | ├── pizza
| | | ├── cover.jpg
| | | ├── index.en.md
| | | ├── index.es.md
│ ├── pages
│ ├── images
Inside our new Spanish markdown files, we will have the same structure in our frontmatter but translated to our new language and change the locale
property in the frontmatter to es
. However, it’s important to note that the slug
field must be the same in each locale.
gatsby-plugin-i18n
This plugin is displayed in Gatsby’s Internationalization Guide as its first option when implementing i18n routes. The purpose of this plugin is to create localized routes by adding a language code in each page filename, so, for example, a ./src/pages/index.en.js
file would result in a my-site.com/en/
route.
I strongly recommend not using this plugin. It is outdated and hasn’t been updated since 2019, so it is kind of a disappointment to see it promoted as one of the main solutions for i18n in Gatsby’s official documentation. It also breaks the File System API, so you must use another method for creating pages, like the createPages
function in the Gatsby Node API. Its only real use would be to create localized routes for certain pages, but considering that you must create a file for each page and each locale, it would be impossible to manage them on even medium sites. A 20 pages site with support for five languages would need 100 files!
gatsby-theme-i18n
Another plugin for implementing localized routes is gatsby-theme-i18n
, which will be pretty easy to use in our prior example.
We will first need to install the gatsby-theme-i18n
plugin and the gatsby-plugin-react-helmet
and react-helmet
plugins to help add useful language metadata in our <head>
tag.
npm install gatsby-theme-i18n gatsby-plugin-react-helmet react-helmet
Next, we can add it to the gatsby-config.js
:
// ./gatsby-config.js
module.exports = {
//...
plugins: [
//other plugins ...
{
resolve: `gatsby-theme-i18n`,
options: {
defaultLang: `en`,
prefixDefault: true,
configPath: require.resolve(`./i18n/config.json`),
},
},
],
};
As you can see, the plugin configPath
points to a JSON file. This file will have all the information necessary to add each locale. We will create it in a new ./i18n/
directory at the root of our project:
[
{
"code": "en",
"hrefLang": "en-US",
"name": "English",
"localName": "English",
"langDir": "ltr",
"dateFormat": "MM/DD/YYYY"
},
{
"code": "es",
"hrefLang": "es-ES",
"name": "Spanish",
"localName": "Español",
"langDir": "ltr",
"dateFormat": "DD.MM.YYYY"
}
]
Note: To see changes in the gatsby-config.js
file, we will need to restart the development server.
And just as simple as that, we added i18n routes to all our pages. Let’s head to http://localhost:8000/es/
or http://localhost:8000/en/
to see the result.
Querying Localized Content
At first glance, you will see a big problem: the Spanish and English pages have all the posts from both locales because we aren’t filtering the recipes for a specific locale, so we get all the available recipes. We can solve this by once again adding variables to our GraphQL queries. The gatsby-theme-i18n
injects the current locale into the page’s context, making it available to use as a query variable under the $locale
name.
index
page query:
query IndexQuery($locale: String) {
allMarkdownRemark(filter: {frontmatter: {locale: {eq: $locale}}}) {
nodes {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
}
}
}
{markdownRemark.frontmatter__slug}.js
page query:
query RecipeQuery($frontmatter__slug: String, $locale: String) {
markdownRemark(frontmatter: {slug: {eq: $frontmatter__slug}, locale: {eq: $locale}}) {
frontmatter {
slug
title
date
cover_image {
image {
childImageSharp {
gatsbyImageData
}
}
alt
}
}
html
}
}
Localizing Links
You will also notice that all Gatsby links are broken since they point to the non-localized routes instead of the new routes, so they will direct the user to a 404 page. To solve this, gatsby-theme-i18n
exposes a LocalizedLink
component that works exactly like Gatsby’s Link
but points to the current locale. We just have to switch each Link
component for a LocalizedLink
.
// ./src/components/RecipePreview.js
+ import {LocalizedLink as Link} from "gatsby-theme-i18n";
- import {Link} from "gatsby";
//...
Changing Locales
Another vital feature to add will be a component to change from one locale to another. However, making a language selector isn’t completely straightforward. First, we will need to know the current page’s path, like /en/recipes/pizza
, to extract the recipes/pizza
part and add the desired locale, getting /es/recipes/pizza
.
To access the page’s location information (URL, HREF, path, and so on) in all our components, we will need to use the wrapPageElement
function available in the gatsby-browser.js
and gatsby-ssr.js
files. In short, this function lets you access the props
used on each page, including a location
object. We can set up a context provider with the location information and pass it down to all components.
First, we will create the location context in a new directory: ./src/context/
.
// ./src/context/LocationContext.js
import * as React from "react";
import {createContext} from "react";
export const LocationContext = createContext();
export const LocationProvider = ({location, children}) => {
return <LocationContext.Provider value={location}>{children}</LocationContext.Provider>;
};
As you can imagine, we will pass the page’s location
object to the provider’s location
attribute on each Gatsby file:
// ./gatsby-ssr.js & ./gatsby-browser.js
import * as React from "react";
import {LocationProvider} from "./src/context/LocationContext";
export const wrapPageElement = ({element, props}) => {
const {location} = props;
return <LocationProvider location={location}>{element}</LocationProvider>;
};
Note: Since we just created the gatsby-ssr.js
and gatsby-browser.js
files, we will need to restart the development server.
Now the page’s location is available in all components through context, and we can use it in our language selector. We have also to pass down the current locale to all components, and the gatsby-theme-i18n
exposes a useful useLocalization
hook that let you access the current locale and the i18n config. However, a caveat is that it can’t get the current locale on Gatsby files like gatsby-browser.js
and gatsby-ssr.js
, only the i18n config.
Ideally, we would want to render our language selector using wrapPageElement
so it is available on all pages, but we can’t use the useLocazication
hook. Fortunately, the wrapPageElement
props
argument also exposes the page’s context and, inside, its current locale.
Let’s create another context to pass down the locale:
// ./src/context/LocaleContext.js
import * as React from "react";
import {createContext} from "react";
export const LocaleContext = createContext();
export const LocaleProvider = ({locale, children}) => {
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
};
And use it in our wrapPageElement
function:
// ./gatsby-ssr.js & ./gatsby-browser.js
import * as React from "react";
import {LocationProvider} from "./src/context/LocationContext";
import {LocaleProvider} from "./src/context/LocaleContext";
export const wrapPageElement = ({element, props}) => {
const {location} = props;
const {locale} = element.props.pageContext;
return (
<LocationProvider location={location}>
<LocaleProvider locale={locale}>{element}</LocaleProvider>
</LocationProvider>
);
};
The last thing is how to remove the locale (es
or en
) from the path (/es/recipes/pizza
). Using the following simple but ugly regex, we can remove all the /en/
and /es/
at the beginning of the path:
/(\/e(s|n)|)(\/*|)/
It’s important to note that the regex pattern only works for the en
and es
combination of locales.
Now we have to create our LanguageSelector.js
:
// ./src/components/LanguageSelector
import * as React from "react";
import {useContext} from "react";
import {useLocalization} from "gatsby-theme-i18n";
import {Link} from "gatsby";
import {LocationContext} from "../context/LocationContext";
import {LocaleContext} from "../context/LocaleContext";
export const LanguageSelector = () => {
const {config} = useLocalization();
const locale = useContext(LocaleContext);
const {pathname} = useContext(LocationContext);
const removeLocalePath = /(\/e(s|n)|)(\/*|)/;
const pathnameWithoutLocale = pathname.replace(removeLocalePath, "");
return (
<div>
{config.map(({code, localName}) => {
return (
code !== locale && (
<Link key={code} to={`/${code}/${pathnameWithoutLocale}`}>
{localName}
</Link>
)
);
})}
</div>
);
};
Let’s break down what is happening:
- Get our i18n config through the
useLocalization
hook. - Get the current locale through context.
- Get the page’s current pathname through context, which is the part that comes after the domain (like
/en/recipes/pizza
). - We remove the locale part of the pathname using a regex pattern (leaving just
recipes/pizza
). - We want to render a link for each available locale except the current one, so we will check if the locale is the same as the page before rendering a common Gatsby
Link
to the desired locale.
Now inside our gatsby-ssr.js
and gatsby-browser.js
files, we can add our LanguageSelector
:
// ./gatsby-ssr.js & ./gatsby-browser.js
import * as React from "react";
import {LocationProvider} from "./src/context/LocationContext";
import {LocaleProvider} from "./src/context/LocaleContext";
import {LanguageSelector} from "./src/components/LanguageSelector";
export const wrapPageElement = ({element, props}) => {
const {location} = props;
const {locale} = element.props.pageContext;
return (
<LocationProvider location={location}>
<LocaleProvider locale={locale}>
<LanguageSelector />
{element}
</LocaleProvider>
</LocationProvider>
);
};
Redirecting Users
The last detail to address is that now the non-i18n routes like http://localhost:8000/
or http://localhost:8000/recipes/pizza
are empty. To solve this, we can redirect the user to their desired locale using Gatsby’s redirect in gatsby-node.js
.
// ./gatsby-node.js
exports.createPages = async ({actions}) => {
const {createRedirect} = actions;
createRedirect({
fromPath: `/*`,
toPath: `/en/*`,
isPermanent: true,
});
createRedirect({
fromPath: `/*`,
toPath: `/es/*`,
isPermanent: true,
conditions: {
language: [`es`],
},
});
};
Note: Redirects only work in production! Not in the local development server.
With this, each page that doesn’t start with the English or Spanish locale will be redirected to a localized route. The wildcard *
at the end of each route says it will redirect them to the same path, e.g., it will redirect /recipes/mac-and-cheese/
to /en/recipes/mac-and-cheese/
. Also, it will check for the specified language in the request’s origin and redirect to the locale if available; else, it will default to English.
react-intl
react-intl
is an internationalization library for any React app that can be used with Gatsby without any extra configuration. It provides a component to handle translations and many more to format numbers, dates, times, etc. Like the following:
FormattedNumber
,FormattedDate
,FormattedTime
.
It works by adding a provider called IntlProvider
to pass down the current locale to all the react-intl
components. Among others, the provider takes three main attributes:
message
An object with all your translations.locale
The current page’s locale.defaultLocale
The default page’s locale.
So, for example:
<IntlProvider messages={{}} locale="es" defaultLocale="en" >
<FormattedNumber value={15000} />
<br />
<FormattedDate value={Date.now()} />
<br />
<FormattedTime value={Date.now()} />
<br />
</IntlProvider>,
Will format the given values to Spanish and render:
15.000
23/1/2023
19:40
But if the locale
attribute in IntlProvider
was en
, it would format the values to English and render:
15,000
1/23/2023
7:42 PM
Pretty cool and simple!
Using react-intl
With Gatsby
To showcase how the react-intl
works with Gatsby, we will continue from our prior example using gatsby-theme-i18n
.
We first will need to install the react-intl
package:
npm i react-intl
Secondly, we have to write our translations, and in this case, we just have to translate the title and subtitle on the index.js
page. To do so, we will create a file called messajes.js
in the ./i18n/
directory:
// ./i18n/messages.js
export const messages = {
en: {
index_page_title: "Welcome to my English cooking blog!",
index_page_subtitle: "Written by Juan Diego Rodríguez",
},
es: {
index_page_title: "¡Bienvenidos a mi blog de cocina en español!",
index_page_subtitle: "Escrito por Juan Diego Rodríguez",
},
};
Next, we have to set up the IntlProvider
in the gatsby-ssr.js
and gatsby-browser.js
files:
// ./gatsby-ssr.js & ./gatsby-browser.js
import * as React from "react";
import {LocationProvider} from "./src/context/LocationContext";
import {LocaleProvider} from "./src/context/LocaleContext";
import {IntlProvider} from "react-intl";
import {LanguageSelector} from "./src/components/LanguageSelector";
import {messages} from "./i18n/messages";
export const wrapPageElement = ({element, props}) => {
const {location} = props;
const {locale} = element.props.pageContext;
return (
<LocationProvider location={location}>
<LocaleProvider locale={locale}>
<IntlProvider messages={messages[locale]} locale={locale} defaultLocale="en">
<LanguageSelector />
{element}
</IntlProvider>
</LocaleProvider>
</LocationProvider>
);
};
And use the FormattedMessage
component with an id
attribute holding the desired translation key name.
// ./src/pages/index.js
// ...
import {FormattedMessage} from "react-intl";
const IndexPage = ({data}) => {
const recipes = data.allMarkdownRemark.nodes;
return (
<main>
<h1>
<FormattedMessage id="index_page_title" />
</h1>
<h2>
<FormattedMessage id="index_page_subtitle" />
</h2>
{recipes.map(({frontmatter}) => {
return <RecipePreview key={frontmatter.slug} data={frontmatter} />;
})}
</main>
);
};
// ...
And as simple as that, our translations will be applied depending on the current user’s locale. However, i18n isn’t only translating all the text to other languages but also adapting to the way numbers, dates, currency, and so on are formatted in the user’s regions. In our example, we can format the date on each recipe page to be formatted according to the current locale using the FormattedDate
component.
// ./src/pages/recipes/{markdownRemark.frontmatter__slug}.js
//...
import {FormattedDate} from "react-intl";
const RecipePage = ({data}) => {
const {html, frontmatter} = data.markdownRemark;
const {title, cover_image, date} = frontmatter;
const cover_image_data = getImage(cover_image.image.childImageSharp.gatsbyImageData);
return (
<main>
<h1>{title}</h1>
<FormattedDate value={date} year="numeric" month="long" day="2-digit" />
<GatsbyImage image={cover_image_data} alt={cover_image.alt} />
<p dangerouslySetInnerHTML={{__html: html}}></p>
</main>
);
};
//...
As you can see, we feed the component the raw date and specify how we want to display it. Then the component will automatically format it to the correct locale. And with the year
, month
, and day
attributes, we can further customize how to display our date. In our example, the date 19-01-2023
will be formatted the following way:
English: January 19, 2023
Spanish: 19 de enero de 2023
If we want to add a localized string around the date, we can use react-intl
arguments. Arguments are a way to add dynamic data inside our react-intl
messages. It works by adding curly braces {}
inside a message.
The arguments follow this pattern { key, type, format }
, in which
key
is the data to be formatted;type
specifies if thekey
is going to be a number, date, time, and so on;format
further specifies the format, e.g., if a date is going to be written like10/05/2023
orOctober 5, 2023
.
In our case, we will name our key postedOn
, and it will be a date
type in a long
format:
// ./i18n/messages.js
export const messages = {
en: {
// ...
recipe_post_date: "Written on {postedOn, date, long}",
},
es: {
// ...
recipe_post_date: "Escrito el {postedOn, date, long}",
},
};
// ./src/pages/recipes/{markdownRemark.frontmatter__slug}.js
//...
import {FormattedMessage} from "react-intl";
const RecipePage = ({data}) => {
const {html, frontmatter} = data.markdownRemark;
const {title, cover_image, date} = frontmatter;
const cover_image_data = getImage(cover_image.image.childImageSharp.gatsbyImageData);
return (
<main>
<h1>{title}</h1>
<FormattedMessage id="recipe_post_date" values={{postedOn: new Date(date)}} />
<GatsbyImage image={cover_image_data} alt={cover_image.alt} />
<p dangerouslySetInnerHTML={{__html: html}}></p>
</main>
);
};
//...
Note: For the date to work, we will need to create a new Date
object with our date
as its only argument.
Localizing The Page’s Title
The last thing you may have noticed is that the index page’s title isn’t localized. In the recipes pages’ case, this isn’t a problem since it queries the already localized title for each post, but the index
page title doesn’t. Solving this can be tricky for two reasons:
- You can’t use Gatsby Head API directly with
react-intl
since theIntlProvider
doesn’t exist for components created inside the Head API. - You can’t use the
FormattedMessage
component inside thetitle
tag since it only allows a simple string value, not a component.
However, there is a workaround for both problems:
- We can use
react-helmet
(which we installed withgatsby-theme-i18n
) inside the page component where theIntlProvider
is available. - We can use
react-intl
imperative API to get the messages as strings instead of theFormmatedMessage
component. In this case, the imperative API exposes auseIntl
hook which returns anintl
object, and theintl.messages
property holds all our messages too.
So the index
component would end up like this:
// ./src/pages/index.js
// ...
import {FormattedMessage, useIntl} from "react-intl";
import {Helmet} from "react-helmet";
const IndexPage = ({data}) => {
const intl = useIntl();
const recipes = data.allMarkdownRemark.nodes;
return (
<main>
<Helmet>
<title>{intl.messages.index_page_title}</title>
</Helmet>
<h1>
<FormattedMessage id="index_page_title" />
</h1>
<h2>
<FormattedMessage id="index_page_subtitle" />
</h2>
{recipes.map(({frontmatter}) => {
return <RecipePreview key={frontmatter.slug} data={frontmatter} />;
})}
</main>
);
};
// ...
react-i18next
react-i18next
is a well-established library for adding i18n to our react sites, and it brings the same and more features, hooks, and utils of react-intl
. However, a crucial difference is that to set up react-i18next
, we will need to create a wrapper plugin in gatsby-node.js
while you can use react-intl
as soon as you install it, so I believe it’s a better option to use with Gatsby. However, there already are plugins to set up faster the react-i18next
library like gatsby-plugin-react-i18next
and gatsby-theme-i18n-react-i18next
.
Conclusion
The current state of Gatsby and especially its plugin is precarious, and each year its popularity goes lower, so it’s important to know how to handle it and which plugins to use if you want to work with Gatsby. Despite all, I still believe Gatsby is a powerful tool and is still worth starting a new project with npm init gatsby
.
I hope you found this guide useful and leave with a better grasp of i18n in Gatsby and with less of a headache. In the next article, we will explore an in-depth solution to i18n by creating your own i18n plugin!