Gatsbyで多言語対応を自動化する(i18n)

Gatsby + Typescript で多言語対応(i18n)

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

各言語用のファイルを用意

ja.json
{
  "blog": "ブログ"
}
en.json
{
  "blog": "Blog"
}

config.ts の作成

下記はi18nを初期化するコードです。resourcesのところでファイルを指定しています。
先程作成した言語ファイルのパスを指定して下さい。

config.ts
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;

React Context で現在の言語を取得

Contextによって、現在の言語の設定をどの階層からも取得できるようにします。

PageContext.jsx
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-config.js に設定を追加

gatsby-plugin-netlifyと、言語の設定を追加します。

gatsby-config.js
require('ts-node').register({ files: true })

module.exports = {
  siteMetadata: {
    ...
    supportedLanguages: ['en', 'ja'],
    defaultLanguage: 'en',
  },
  plugins: [
    ...
    `gatsby-plugin-netlify`,
  ],
}

gatsby-node.js の準備

こちらで各言語ごとのファイルを生成するように記述します。

gatsby-node.js
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,
  });
};

gatsby-ssr.js の準備

各エレメントをContextでWrapします。

gatsby-ssr.js
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>
  );
};

gatsby-browser.js の準備

gatsby-browser.js
/**
 * 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";

seo 対応

React helmetに以下のように設定します。

Seo.tsx
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 を i18n へ対応

Linkに渡すパスを以下のように設定します。今回はLinkは使いませんが、もし必要なりましたら、下記コードを参考にして下さい。

Link.tsx
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;

_redirects の中身を見てみる

ここまで来たら準備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

i18n を使って文字を表示してみる

index.tsx
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/と入力すると日本語に切り替わります。
ポート番号はお使いの環境に合わせて下さい。

blog-ja-image

動作確認

netlifyへデプロイすれば、自動でブラウザの言語を理解してリダイレクトしてくれます。
なお、netlifyのバグっぽいのですが、ブラウザの言語に複数設定している場合redirectが正しく動作しないようです。
ローカルでは自動判定はしないようです。デバッグする場合は、ブラウザのURLに直接入力する必要があります。


Written by Yasuhiro Ito
Software engineer

© 2021, Yasuhiro Ito