Next.js で国際化(i18n)対応サイトを作る

f:id:goodpatch-tech:20210719183325p:plain

Design Division 所属 Webフロントエンドエンジニアの 上垣 です。 普段の業務ではデザイナーと協力して、良いデザインでクライアントのビジネスを前進させるために、ひたすら Webフロントエンドの実装をしています。

一方で、Goodpatch のエンジニアグループでは、月の業務時間の最大20%を、社内プロジェクトや自分の興味がある分野を探求する時間として使うことが推奨されています。この時間を利用した私の最近の取り組みとして、Next.js で 日本語/英語 に対応するサイトを作成したので、そこで得た知見をこの記事で紹介します。

やりたかったこと

今回作ったサイトの、国際化に関する大まかな要件は下記のとおりです。

  • 日本語と英語に対応する。
  • 自前で翻訳データを作る。 (翻訳 SaaS を使わない)
  • ページデータは API, データベースに依存しない。

Next.js 自体は翻訳の機能は持ちませんが、Ver 10.0 から導入された internationalized routing を利用することで、比較的簡単に、適切なロケール判定とそれに対応したルーティングを実装できることがわかったので、Next.js を採用しました。

Next.js バージョン

11.0.1

ルーティングを設定する

Next.js では、国際化ルーティングの方法として、下記の2つの方法をサポートしています。

  1. Sub-path Routing

    URLの path にロケールを含める 例: https://example.com/en/home

  2. Domain Routing

    ローケールごとにドメインを割り振る 例: https://example.en/home

今回は、よりシンプルに実装できる Sub-path Routing を採用しました。 Sub-path Routing を設定する next.config.js は、下記のとおりです。

module.exports = {
  i18n: {
    defaultLocale: 'ja',
    locales: ['ja', 'en']
  }
};

defaultLocale で ja を指定し、ロケールを指定しないか、適切なロケールが存在しない場合は、日本語のページが表示されるようにしています。

コンポーネントからロケール情報を取得する

コンポーネントから現在のロケールや、選択可能なロケールを取得するには、useRouter フックを使います。

import { useRouter } from "next/router";

export default function Home() {
  const { locale, locales, defaultLocale } = useRouter();
  return <div>Current locale is {locale}.</div>;
}

ページ生成時に依存する外部データの取得にロケール情報が必要な場合は、getStaticProps や getServerSideProps に渡される context オブジェクトから、locale にアクセスできます。

export const getStaticProps = async ({ locale = 'ja' }) => {
  const dataByLocale = await getDataFromApi({locale});

  return {
    props: {
      data: dataByLocale
    }
  };

Next.js のルーティング判定を検証する

Next.js はリクエストヘッダーの Accept-Language で適切なロケールを判定している(リンク)ので、/ ページにアクセスする際にヘッダーの値を変えながら、ルーティングがどのように動作するか検証してみます。

リクエストの Accept-Language に en が含まれる場合

GET / HTTP/1.1
Host: localhost:3000
User-Agent: insomnia/2021.4.1
Accept-Language: en-US,en;q=0.5
Accept: */*

ステータスコード307 が返され、Location に /en が指定されました。

HTTP/1.1 307 Temporary Redirect
Location: /en
Date: Mon, 19 Jul 2021 01:24:38 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

リクエストの Accept-Language に ja が含まれる場合

GET / HTTP/1.1
Host: localhost:3000
User-Agent: insomnia/2021.4.1
Accept-Language: ja,en-US,en;q=0.5
Accept: */*

リダイレクトせずに、正常にレスポンスが返されました。

HTTP/1.1 200 OK
X-Powered-By: Next.js
ETag: "5a9-1GsX/pSH+NFTxyCHvNVRmtIo3oE"
Content-Type: text/html; charset=utf-8
Content-Length: 1449
Vary: Accept-Encoding
Date: Mon, 19 Jul 2021 01:40:12 GMT
Connection: keep-alive
Keep-Alive: timeout=5

なお、NEXT_LOCALE Cookie にlocale をセットすると、 Accept-Language で要求されたロケールを上書きできます。 ユーザーが任意のロケールを選択できるようにする場合は、nookies などを使って Cookie で永続化すると良いでしょう 。

翻訳ライブラリと組み合わせる

以上で、リクエストが要求する言語に対して、適切なページを表示させることができるようになりました。後は翻訳ライブラリと組み合わせて、言語別のコンテンツを作成していきます。 今回は、設定がシンプルな i18n-next の Next.js 用ライブラリ next-i18next を使って、翻訳機能を実装しました。

next-i18nextバージョン

8.5.5

翻訳ファイルを作成する

翻訳ファイルは、json 形式で /public/locales/{ロケール名} ディレクトリ内に配置していきます。

ファイル名がそのまま namespace となるので、まずはデフォルトの common という名前で、ファイルを作成しました。翻訳ワードが増えて管理が煩雑になってきたら、ページやドメイン単位でファイルを分割していくと良いと思います。

// /public/locales/ja/common.json
{
  "title": "グッドパッチ テックブログ"
}

// /public/localos/en/common.json
{
  "title": "Goodpatch Tech Blog"
}

実装

翻訳ファイルが作成できたら、後は next-i18next のドキュメントどおりに実装を進めていきます。

1.appWithTranslation HOC で _app.tsx をラップする

// /pages/_app.tsx
import { appWithTranslation } from "next-i18next";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}
export default appWithTranslation(MyApp);

2.各ページの getStaticProps (or getServerSideProps) で、serverSideTranslations を実行し、辞書データをprops としてコンポーネントに渡す

// /pages/index.tsx
import { GetStaticProps } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

export default function Home() {
  return <h1></h1>;
}

export const getStaticProps: GetStaticProps = async ({ locale = "ja" }) => {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["common"]))
    }
  };
};

3.useTranslation フックで取得できる t 関数で、翻訳を実行する

// /pages/index.tsx
import { useTranslation } from "next-i18next";

export default function Home() {
  const { t } = useTranslation("common");
  return <h1>{t("title")}</h1>;
}

// 省略

実行結果

http://localhost:3000

f:id:goodpatch-tech:20210721090319p:plain

http://localhost:3000/en

f:id:goodpatch-tech:20210721090449p:plain

まとめ

Next.js の国際化ルーティング機能を使うと、簡単な設定で言語別のページを表示できることがわかりました。 言語別ページの事前生成にも対応できるので、小規模なサイトなら、SaaS などを利用しなくても十分に対応可能だと思います。

国際化に限らず、Next.js は少ない設定でやりたいことを実現できる機能が揃っていて、良いアプリケーションの設計としてとても勉強になるなと改めて感じました。


Goodpatch には、デザインと技術の両方を追求できる環境があります。 少しでもご興味を持ってくださった方、ぜひ一度カジュアルにお話ししましょう!