Dmytro Rykhlyk
Posted on March 6, 2021
Recently, working on a Gatsby.js project, I had to implement a multi-language (internationalization / i18n) support. A lot of guides are out there, using different approaches/tools, but in this article, I want to show the solution I ended up using and go through the problems that Iβve faced.
So what do we need to implement and what we want to achieve?
- Keep configuration and translations in a centralized way.
- Generate language-specific static pages.
- Localize URLs.
- Identify user locale and update components.
- Implement language switcher buttons to alter between the languages.
- Use no external dependencies (or as few as possible).
Take a look at the Github repo with example project and the DEMO
π Generate pages for each locale
Before we jump into code, we need to think about localizing URLs. In this example, let's assume that the default language should have no prefix in the URL. We'll add English as a default language and Japanese as a second one.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Languages β index.js β page-2.js β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β English (default) β / β /page-2 β
β Japanese β /ja β /ja/page-2 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Letβs create a module in the locales.js
file that would contain all locales definitions. Note that onlyΒ oneΒ languageΒ shouldΒ haveΒ theΒ default:Β true
Β key.
// i18n/locales.js
module.exports = {
en: {
path: 'en',
locale: 'English',
default: true,
},
ja: {
path: 'ja',
locale: 'ζ₯ζ¬θͺ',
},
};
Instead of manually writing different components for each language, we can add the following content to our gatsby-node.js
file, where onCreatePage
hook will create a page for each locale and pass its locale
and isDefault
props to the page context:
// gatsby-node.js
const locales = require('./i18n/locales');
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions;
// For each page, weβre deleting it, than creating it again for each
// language passing the locale to the page context
return new Promise(resolve => {
deletePage(page);
Object.keys(locales).map(lang => {
const isDefault = locales[lang].default || false;
const localizedPath = isDefault
? page.path
: locales[lang].path + page.path;
return createPage({
...page,
path: localizedPath,
context: {
locale: lang,
isDefault,
},
});
});
resolve();
});
};
If you go to http://localhost:8000/___graphql
and query all pages you should see automatically generated pages for each locale:
Now you can pass pageContext
to the layout component to be able to use locale
and isDefault
later.
{
allSitePage {
edges {
node {
path
context {
isDefault
locale
}
}
}
}
}
π Add translations in JSON format
Letβs place some data in the i18n/translations/
folder using JSON format following the defined language conventionon - adding suffix to the filename that contain the language code [name].[language].json
, like data.en.json
or data.js.json
.
First, we need to be sure that our project includes these dependencies:
- gatsby-transformer-json - to get content from JSON files.
- gatsby-source-filesystem - to query files inside
/i18n/
folder.
yarn add gatsby-source-filesystem gatsby-transformer-json
and then update gatsby-config.js
configuration:
// gatsby-config.js
module.exports = {
// ...
plugins: [
'gatsby-transformer-json',
{
resolve: `gatsby-source-filesystem`,
options: {
name: `i18n`,
path: `${__dirname}/i18n`,
},
},
// ...
],
};
Letβs create some translations for both languages:
// i18n/translations/en.json
{
"title": "Gatsby Default Starter",
"greeting": "Hi people",
// ...
}
// i18n/translations/ja.json
{
"title": "Gatsbyγγγ©γ«γγΉγΏγΌγΏγΌ",
"greeting": "ηγγγγγγ«γ‘γ―",
// ...
}
π useLocale hook for keeping the current language
Gatsby uses @reach/router
under the hood, so we can get access to the current pathname
to be able to extract a language name from the URL and use it on app initialization, so we don't need to store any initial data in the localStorage or elsewhere.
We can take advantage of react's Context API to share current language value between components.
// src/hooks/useLocale.js
import React, { createContext, useState, useContext } from 'react';
import { useLocation } from '@reach/router';
import allLocales from '../../i18n/locales';
const LocaleContext = createContext('');
const LocaleProvider = ({ children }) => {
const { pathname } = useLocation();
// Find a default language
const defaultLang = Object.keys(allLocales)
.filter(lang => allLocales[lang].default)[0];
// Get language prefix from the URL
const urlLang = pathname.split('/')[1];
// Search if locale matches defined, if not set 'en' as default
const currentLang = Object.keys(allLocales)
.map(lang => allLocales[lang].path)
.includes(urlLang)
? urlLang
: defaultLang;
const [locale, setLocale] = useState(currentLang);
const changeLocale = lang => {
if (lang) {
setLocale(lang);
}
};
return (
<LocaleContext.Provider value={{ locale, changeLocale }}>
{children}
</LocaleContext.Provider>
);
};
const useLocale = () => {
const context = useContext(LocaleContext);
if (!context) {throw new Error('useLocale must be used within an LocaleProvider');}
return context;
};
export { LocaleProvider, useLocale };
Our hook returns LocaleProvider
, so we can add it to wrap our layout component:
// src/components/app.js
import { LocaleProvider } from '../hooks/useLocale';
import Layout from './layout';
const App = ({ children, pageContext: { locale, isDefault } }) => (
<LocaleProvider>
<Layout locale={locale} isDefault={isDefault}>
{children}
</Layout>
</LocaleProvider>
);
// ...
And then, we can use changeLocale
function to provide a way to change the language using buttons. Adding next code we can be sure that a new value will be stored every time the locale
value changes:
// src/components/layout.js
import { useLocale } from '../hooks/useLocale';
const Layout = ({ children, pageContext: { locale, isDefault } }) => {
const { changeLocale } = useLocale();
// Every time url changes we update our context store
useEffect(() => {
changeLocale(locale);
}, [locale]);
// ...
};
π useTranslation hook for querying data
Thereβs a drawback with the approach of using useStaticQuery
- static queries do not take variables, so we need to manually write all query nodes.
We'll create a helper function to first simplify query response and then filter by current language.
// src/hooks/useTranslation.js
import { useStaticQuery, graphql } from 'gatsby';
import { useLocale } from './useLocale';
const query = graphql`
query useTranslations {
allFile(filter: {relativeDirectory: {eq: "translations"}}) {
edges {
node {
name
childrenTranslationsJson {
greeting
mainPageContent
secondPageLink
title
goHomeLink
secodPageContent
secondGreeting
indexPageTitle
secondPageTitle
NotFoundPageTitle
NotFoundPageContent
}
}
}
}
}
`;
// This hook simplifies query response for current language.
const useTranslation = () => {
const { locale } = useLocale();
const { allFile } = useStaticQuery(query);
// Extract all lists from GraphQL query response
const queryList = allFile.edges.map(item => {
const currentFileTitle = Object.keys(item.node).filter(
item => item !== 'name',
)[0];
return {
name: item.node.name,
...item.node[currentFileTitle][0],
};
});
// Return translation for the current locale
return queryList.filter(lang => lang.name === locale)[0];
};
export default useTranslation;
Now we can use our translations from the i18n/translations/
data in our components:
// src/pages/page-2.js
// ...
import SEO from '../components/seo';
import useTranslation from '../hooks/useTranslation';
const SecondPage = ({ pageContext }) => {
const {
secodPageContent,
goHomeLink,
secondGreeting,
secondPageTitle,
} = useTranslation();
return (
<>
<SEO title={secondPageTitle} />
<h1>{secondGreeting}</h1>
<p>{secodPageContent}</p>
{/* ... */}
</>
);
};
// ...
π Closing Notes
Finally, we'll need to allow the user to correctly browse the site and alternate between both languages. That should be done using localized links and button switcher. You can view them all in this Github repo.
Our published project using Gatsby's default starter
The i18n implementation we end up with uses no external dependencies (or as few as possible): Context API, hooks, Gatsbyβs createPage()
in the gatsby-node.js
to generate pages for each locale, gatsby-transformer-json
plugin to manage translation data.
Take a look at the Github repo with example project and the DEMO
Links
π Thanks for reading :)
Posted on March 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.