February 10, 2021
Gatsbyでi18n対応する方法を紹介します。
手動で頑張って対応していく方法もありますが、この記事では極力自動化することを念頭に置いております。
urlのパスから、netlify上でredirectをする方法まで紹介します。
なお、こちらで紹介しているソースコードはこちらのサイトを参考にしました。
How to approach multi-language Gatsby apps
一部動作しないところがあったので、修正しています。
GatsbyはSSGなので、ビルド時にファイルをすべて用意する必要があります。gatsby-plugin-i18nを使うと、各言語用にファイルを用意することになります。 しかしながら、ミスが出る上に、いちいちファイルを用意するのは面倒です。そこで、自動的に言語ごとに各ページをビルドするように、gatsby-node.js/gatsby-ssr.jsを作成します。 netlifyは_redirectsというファイルを元にリダイレクトが定義できます。gatsbyのプラグインで_redirectを生成してくれるものがあるのでそれも導入します。
まずは必要なパッケージをインストールします。
yarn add i18next react-i18next gatsby-plugin-netlify
私のディレクトリ構造は以下です。お使いの環境に合わせていただいてかまいせん。
-- src/
-- components/
-- Link.tsx
-- Seo.tsx
-- i18n/
-- config.ts
-- PageContext.jsx
-- locales/
-- ja.json
-- en.json
-- pages/
-- index.tsx
-- gatsby-config.js
-- gatsby-node.js
-- gatsby-ssr.js
-- package.json
{
"blog": "ブログ"
}
{
"blog": "Blog"
}
下記はi18nを初期化するコードです。resourcesのところでファイルを指定しています。
先程作成した言語ファイルのパスを指定して下さい。
import i18next from "i18next";
i18next.init({
fallbackLng: "en",
resources: {
ja: {
translations: require("./locales/ja.json"),
},
en: {
translations: require("./locales/en.json"),
},
},
ns: ["translations"],
defaultNS: "translations",
returnObjects: true,
debug: process.env.NODE_ENV === "development",
interpolation: {
escapeValue: false, // not needed for react!!
},
react: {
wait: true,
},
});
i18next.languages = ["ja", "en"];
export default i18next;
Contextによって、現在の言語の設定をどの階層からも取得できるようにします。
import React from "react";
import { useTranslation } from "react-i18next";
const PageContext = React.createContext({});
export const usePageContext = () => React.useContext(PageContext);
export const PageContextProvider = ({ value, children }) => {
const { i18n } = useTranslation();
if (i18n.language !== value.lang) {
i18n.changeLanguage(value.lang);
}
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;
};
gatsby-plugin-netlifyと、言語の設定を追加します。
require('ts-node').register({ files: true })
module.exports = {
siteMetadata: {
...
supportedLanguages: ['en', 'ja'],
defaultLanguage: 'en',
},
plugins: [
...
`gatsby-plugin-netlify`,
],
}
こちらで各言語ごとのファイルを生成するように記述します。
require("ts-node").register({ files: true });
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/
// You can delete this file if you're not using it
const config = require("./gatsby-config");
/**
* Makes sure to create localized paths for each file in the /pages folder.
* For example, pages/404.js will be converted to /en/404.js and /el/404.js and
* it will be accessible from https:// .../en/404/ and https:// .../el/404/
*/
exports.onCreatePage = async ({
page,
actions: { createPage, deletePage, createRedirect },
}) => {
const isEnvDevelopment = process.env.NODE_ENV === "development";
const originalPath = page.path;
// Delete the original page (since we are gonna create localized versions of it) and add a
// redirect header
await deletePage(page);
await Promise.all(
config.siteMetadata.supportedLanguages.map(async (lang) => {
const localizedPath = `/${lang}${page.path}`;
// create a redirect based on the accept-language header
createRedirect({
fromPath: originalPath,
toPath: localizedPath,
Language: lang,
isPermanent: false,
redirectInBrowser: isEnvDevelopment,
statusCode: 301,
});
await createPage({
...page,
path: localizedPath,
context: {
...page.context,
originalPath,
lang,
},
});
})
);
// Create a fallback redirect if the language is not supported or the
// Accept-Language header is missing for some reason
createRedirect({
fromPath: originalPath,
toPath: `/${config.siteMetadata.defaultLanguage}${page.path}`,
isPermanent: false,
redirectInBrowser: isEnvDevelopment,
statusCode: 301,
});
};
各エレメントをContextでWrapします。
import React from "react";
import { PageContextProvider } from "./src/i18n/PageContext";
import i18n from "@/i18n/config";
import { I18nextProvider } from "react-i18next";
/**
* Wrap all pages with a Translation provider and set the language on SSR time
*/
export const wrapRootElement = ({ element }) => {
return <I18nextProvider i18n={i18n}>{element}</I18nextProvider>;
};
/**
* Wrap all pages with a Translation provider and set the language on SSR time
*/
export const wrapPageElement = ({ element, props }) => {
return (
<PageContextProvider value={props.pageContext}>
{element}
</PageContextProvider>
);
};
/**
* Implement Gatsby's Browser APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/browser-apis/
*/
// You can delete this file if you're not using it
export { wrapPageElement, wrapRootElement } from "./gatsby-ssr";
React helmetに以下のように設定します。
import React from 'react'
import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby'
import { usePageContext } from '../i18n/PageContext'
interface Meta {
name: string
content: string
}
interface Props {
description?: string
lang?: string
meta?: Meta[]
title: string
}
const SEO = ({ description = '', meta = [], title }: Props) => {
const { site } = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
description
author
}
}
}
`)
// Get lang from context!!
const { lang } = usePageContext()
const metaDescription = description || site.siteMetadata.description
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
export default SEO
Linkに渡すパスを以下のように設定します。今回はLinkは使いませんが、もし必要なりましたら、下記コードを参考にして下さい。
import React from "react";
import { Link as GatsbyLink } from "gatsby";
import { usePageContext } from "../i18n/PageContext";
const Link = ({ to, ...rest }) => {
const { lang } = usePageContext();
return <GatsbyLink {...rest} to={`/${lang}${to}`} />;
};
export default Link;
ここまで来たら準備OKです。gatsby buildを実行すると、publicディレクトリ(ビルド成果物が出力されるディレクトリ)に_redirectsというファイルができています。
## Created with gatsby-plugin-netlify
/404/ /en/404/ 301 Language=en
/404/ /ja/404/ 301 Language=ja
/blog/ /en/blog/ 301 Language=en
/blog/ /ja/blog/ 301 Language=ja
/contact/ /en/contact/ 301 Language=en
/contact/ /ja/contact/ 301 Language=ja
/ /en/ 301 Language=en
/ /ja/ 301 Language=ja
/lab/ /en/lab/ 301 Language=en
/lab/ /ja/lab/ 301 Language=ja
/404/ /en/404/ 301
/blog/ /en/blog/ 301
/contact/ /en/contact/ 301
/ /en/ 301
/lab/ /en/lab/ 301
import React from "react";
import { useTranslation } from "react-i18next";
const IndexPage = () => {
const [t] = useTranslation();
return <h1>{t("blog")}</h1>;
};
export default IndexPage;
gatsby developで起動し、ブラウザのurlへlocalhost:8000/ja/と入力すると日本語に切り替わります。
ポート番号はお使いの環境に合わせて下さい。
netlifyへデプロイすれば、自動でブラウザの言語を理解してリダイレクトしてくれます。
なお、netlifyのバグっぽいのですが、ブラウザの言語に複数設定している場合redirectが正しく動作しないようです。
ローカルでは自動判定はしないようです。デバッグする場合は、ブラウザのURLに直接入力する必要があります。
Written by Yasuhiro Ito
Software engineer