There are 24 official languages in the European Union. Creating a Gatsby.js website in so many languages is an edge case for which the guide on localization and internationalization with Gatsby.js might not help.
In this article I’ll reflect on the topic of i18n with Gatsby.js, the available plugins and core APIs. Mostly, how to use core APIs to create a scalable system for delivering multilingual static sites.
In my opinion, when evaluating approaches/tools for multilingualism with Gatsby.js, the solution should:
In short, the tool or the approach taken to solve the issue should be maintenable.
When working on a Gatsby.js project, a research phase would usually consist of browing the following sources:
When it comes to researching for “i18n”:
Trying to get information about the first one gatsby-plugin-i18n
, it leads to Github search https://github.com/search?q=gatsby-plugin-i18n
which shows several repositories, some of them are forks of a very close relation.
The first one angeloocana/gatsby-plugin-i18n uses react-intl
and i18next
. Looks promising with over 210 stars and separate packages for per-topic solutions. However, it automatically goes out of the shortlist - it will not be ok to have 24 files for every single page. Imagine a site with 10 pages which will explode to managing 240 files for a simple site!
The second one ikhudo/gatsby-i18n-plugin also uses i18next
. Has an unofficial? mirror at hupe1980/gatsby-i18n. Looking at code of gatsby-i18n
and gatsby-plugin-i18next
packages we see that documentation is scarce and both haven’t been updated very frequently.
That’s confusing: gatsby-plugin-18
vs gatsby-18n-plugin
, first being a “no-no” and second one being “can’t start the starters”. 🤔
The third plugin in the list is wiziple/gatsby-plugin-intl
uses react-intl
. Shares approach and issues mentioned in angeloocana/gatsby-plugin-i18n
: we can’t afford per-language page.
Lastly, checking whether any of the top (most downloaded) plugins is used in a core plugin or an example: the answer is no.
It’s easy to see that react-intl
and i18next
are the go-to solutions in terms of using i18n frameworks, though at the same time
top plugins are either not scalable or are not production-ready.
Coming back to wiziple/gatsby-plugin-intl
plugin which has a special WHY section:
When you build multilingual sites, Google recommends using different URLs for each language version of a page rather than using cookies or browser settings to adjust the content language on the page.
Looking at the example of using i18n it says:
Example site that demonstrates how to build Gatsby sites with multiple languages (Internationalization / i18n) without any third-party plugins or packages. Per language a dedicated page is built (so no client-side translations) which is among other things important for SEO.
Having these and the awareness about the state of i18n plugins, it’s natural to start on a new path of thinking: HOW to solve the problem without plugins?
From the previous section we reached a point where we know that we can achieve i18n with functions from Gatsby.js’s core. Let’s make a short analysis of the reference example.
Starting from gatsby-node.js
, the file where we can use Gatsby Node APIs, we see usage of createPage()
:
Object.keys(locales).map((lang) => {
// Use the values defined in "locales" to construct the path
const localizedPath = locales[lang].default
? page.path
: `${locales[lang].path}${page.path}`;
return createPage({
// Pass on everything from the original page
...page,
// Since page.path returns with a trailing slash (e.g. "/de/")
// We want to remove that
path: removeTrailingSlash(localizedPath),
// Pass in the locale as context to every page
// This context also gets passed to the src/components/layout file
// This should ensure that the locale is available on every page
context: {
...page.context,
locale: lang,
},
});
});
This is already solving our issue with scalability, because we can define a list of 24 languages and loop through them, having a separate page for each language without creating physical files.
The part about context
and locale
is an example of how to pass a variable to GraphQL queries in Gatsby.js, which can be seen in code here.
At the same time, passing data to context
is related to how Gatsby.js creates pages. For example, the public/page-data/de/page-data.json
of the example will contain:
{
"componentChunkName": "component---src-pages-index-js",
"path": "/de",
"webpackCompilationHash": "ed7057ec19fc05f78011",
"result": {
"data": {},
"pageContext": {
"isCreatedByStatefulCreatePages": true,
"locale": "de",
"dateFormat": "DD.MM.YYYY"
}
}
}
I’ve skipped result.data
to focus on result.pageContext.locale
😉.
The example implementation is already clearly stating:
Usage of a custom hook with GraphQL to access translations. That part can be replaced with a i18n library
This means that the useTranslations()
having the following implementation might need reconsideration:
const query = graphql`
query useTranslations {
rawData: allFile(filter: { sourceInstanceName: { eq: "translations" } }) {
edges {
node {
name
translations: childTranslationsJson {
hello
subline
backToHome
}
}
}
}
}
And you might already see a few potential drawbacks of this approach:
Using sourcing and GraphQL for pulling data into components demonstrates a good pattern of enabling usage of data on SSR, though it might not be the most scalabale approach for the long term.
Let’s try to implement an i18n library in the example. For the demonstration we’ll select i18next.
Following the quick start, the example would have a basic implementation like this
NB: gatsby-plugin-layout
is used for convenience and simplification, not directly related to integration of i18next.
At this stage, a component can use useTranslation
in the following way:
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import { LocaleContext } from "../layouts";
const Welcome = () => {
const { locale } = useContext(LocaleContext);
const { t, i18n } = useTranslation();
i18n.changeLanguage(locale);
return <div>{t("Using i18next")}</div>;
};
export default Welcome;
Yes, the changeLanguage()
will be taken out to be more generic, as well as LocaleContext
does not need stay in the layout
any more, but we have a working example with 2 ways of translating content.
As a first step of refactoring the current implementation, we can do the following:
resources
from the configuration file, as suggested in i18next quick start.I18nextProvider
to pass i18n
instance down to children, rather than relying on use(initReactI18next)
middleware.useEffect(() => {
i18n.changeLanguage(locale);
}, [locale]);
import React from "react";
import { useTranslation } from "react-i18next";
const Welcome = () => {
const { t } = useTranslation();
return <div>{t("Using i18next")}</div>;
};
export default Welcome;
We still make use of the Context API and we are able to access both location
and i18n
from components’, regardless of their location in the hierarchy. No props drillin’.
However, after moving resources
to an external file and import
-ing it, we end up with:
Which is not ideal, because although we see translations in the browser on language switching, the resulting HTML pages are not having the translations in their corresponding languages, but in defaults. Translation happens client-side.
After second refactoring:
localeContext
is taken out from the layout component and moved to a separate file in order to avoid circular dependencies.resources
are require
-ed in gatsby-node.js
and information is passed through context
again. Example: public/page-data/de/page-data.json
{
"componentChunkName": "component---src-pages-index-js",
"path": "/de",
"webpackCompilationHash": "723c1b4c311ddaa3bf91",
"result": {
"data": {},
"pageContext": {
"isCreatedByStatefulCreatePages": true,
"locale": "de",
"localeResources": {
"translation": { "Using i18next": "Using i18next (DE)" }
},
"dateFormat": "DD.MM.YYYY"
}
}
}
i18n
instance and locale
to children. It’s inspired by this).The multilingual setup we end up with uses core functionalities without plugins: i18next, React patterns (HOC), Context API, hooks, and Gatsby’s createPage()
, which facilitate the data management.
Plugins do have their role into solving problems when they are in a specific scope or when they provide enough flexibility in terms of implementation. I think the reason the i18n plugins won’t be the best fit for all types of multilingual sites is that they make assumptions which impose constraints on scalability of the project using them.
I hope this was a useful read for getting the way of thinking rather than the framework specifics.