🌏

Next.jsでi18n対応する方法

2022/12/18に公開

はじめに

株式会社マイベストでフロントエンドエンジニアをしているteppeitaです。

マイベストは、海外でも8カ国でサービスを展開しています。(海外だけで合計MAU1,000万ほどのアクセスがあります)
これまで海外のシステムは国内のものとは別運用だったのですが、国内のビジネスオペレーションを海外にも展開していくために、国内のシステムと統合するプロジェクト(通称グローバル化PJT)が始動しました。

フロントエンドにNext.jsを導入しているため、Next.jsでのi18n対応が必要になり、色々と調査して実装したので、その話をまとめます。

i18n対応とは

i18nとは、internationalizationの略で、iとnの間に18文字あることからi18nと略式で呼ばれています。
internationalizationとは、国・地域による出し分けの仕組みを作ることです。
出し分けは言語の翻訳が主ですが、それだけではなく、例えば弊社だと広告系の仕組みや全文検索エンジンなどを国別に分ける対応も有りました。
(※本記事では言語の翻訳以外の話は割愛しています)

Next.jsのi18n対応について

built-inサポート

Next.jsはbuilt-inでi18nに対応しています。
https://nextjs.org/docs/advanced-features/i18n-routing

ただし、built-inで対応できるのは、routingによる言語の判定のみで、localizationの仕組みは他のライブラリを使う前提になっています。
次に紹介するライブラリのREADMEにも分かりやすくまとまっていました。

Although Next.js provides internationalised routing directly, it does not handle any management of translation content, or the actual translation functionality itself. All Next.js does is keep your locales and URLs in sync.

https://github.com/i18next/next-i18next

built-inのroutingについては、以下のように設定をします。

// next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'nl-NL'],
    defaultLocale: 'en-US',
    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'en-US',
      },
      {
        domain: 'example.nl',
        defaultLocale: 'nl-NL',
      },
    ],
  },

すると、/en-US/users/1のようなパスと、example.com/users/1のようなドメインの両方で、en-US設定の画面にアクセスできます。

また、useRouter hookを使うなどしてlocale情報を取得できます。

import { useRouter } from 'next/router'

const APP = () => {
  const { locale } = useRouter() // en-USのようなstringが入っている
  ...
}

が、実際に実装する際は、多くの場合useRouterではなく、後述の翻訳ライブラリ側のhookを使うことになります。

ライブラリを使う

localeに応じて翻訳を出し分けるためにライブラリを使います。
弊社では、next-i18nextを導入したので、このライブラリについて説明します。
https://github.com/i18next/next-i18next

名前にnextが2つ入っておりややこしい(最初見た時は脳が混乱しました🤯)ですが、
まず、i18nextという大元のライブラリがあり、そのReact版であるreact-i18nextがあり、それをラップする形でNext.js対応したものがnext-i18nextです。

next-i18nextを使った実装

設定方法

ライブラリのREADMEの通りなのですが、まず、configファイルを用意します。
configファイルの書き方は先ほど見たbuilt-inの設定と同じです。

// next-i18next.config.js
module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'de'],
  },
}
// next.config.js
const { i18n } = require('./next-i18next.config')

module.exports = {
  i18n,
}

※next.config.jsに直接書いても良いのですが、i18nの設定が肥大化することを見越して別ファイルに書くことが推奨されているのだと思います。

続いて、翻訳ファイルが読み込まれるように設定していきます。

// _app.tsx
import { appWithTranslation } from 'next-i18next'

const MyApp = ({ Component, pageProps }) => (
  <Component {...pageProps} />
)

export default appWithTranslation(MyApp)

そして、各コンポーネントで以下のようにpropsを渡す必要が有ります。(SSRでない場合はgetStaticProps)

import { serverSideTranslations } from 'next-i18next/serverSideTranslations'

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, [
        'common',
      ])),
    },
  }
}

翻訳ファイルの本体は以下のように配置します。

.
└── public
    └── locales
        ├── en
        |   └── common.json
        └── de
            └── common.json

これで翻訳の設定は完了です。

翻訳の実装例

翻訳は以下のように実装します。

// public/locales/en/common.json
{
  "title": "This is title"
}
import { useTranslation } from 'next-i18next'

export const Title = () => {
  const { t } = useTranslation('common')

  return (
    <h1>
      {t('title')} // "This is title"
    </h1>
  )
}

翻訳ファイルはcommon.json以外にも用意することができます。
(※common.jsonは削除するとエラーになるため必須です)

.
└── public
    └── locales
        └── en
            ├── common.json
            └── footer.json
  const { t } = useTranslation('footer')

また、jsonファイル内でネストすることも可能です。

// public/locales/en/common.json
{
  "article": {
    "title": "This is Article",
    "author": {
      "name": "Pochita"
    }
  }
}
  {t('article.author.name')} // Pochita

これらの仕組みを利用して、サービスに合わせたルールを決めて構造化すると運用しやすいでしょう。

テンプレート単位での出しわけたい時

staticなページなどで、1文1文出し分けるのが手間だったり、国によっては微妙にHTMLの構造から変えたい場合などにテンプレート単位で分けたいケースが有ります。

そんな時はdynamic importが使えると綺麗に実装できそうです。

import dynamic from 'next/dynamic';

export const BodyByLocale = ({ locale }) => {
  const Body = dynamic(() => import(`./Body.${locale}`);
  return <Body />
}

しかし、残念ながら、Next.jsのdynamic importは、ファイル名を動的に指定できません 😢

In import('path/to/component'), the path must be explicitly written. It can't be a template string nor a variable.

https://nextjs.org/docs/advanced-features/dynamic-import

そこで、弊社では愚直にswitch文で出しわけて、その中でdynamic importしています。

  let Body = null;

  switch (locale) {
    case 'ja':
      Body = dynamic(() => import('./Body.ja'));
      return <Body />;
    case 'en-US':
      Body = dynamic(() => import('./Body.en-US'));
      return <Body />;
    default:
      return null;
  }

これによって、Body.ja.tsxBody.en-US.tsxの中身を自由に書き換えて出し分けることができています。
(※もっと良いやり方が有るんじゃないかと模索中です)

まとめ

Next.jsのi18n対応は、調査した当初はbuilt-inでどこまでできるのか、ライブラリは必要なのか、など不明点が多く地味に大変でした。
しかし一通り調べてみると、next-i18nextを導入して、ライブラリのやり方に従えば良いことが分かり、シンプルな形に落ち着きました。

また、今回は割愛しましたが、他にも色んな課題にぶつかりながら実装を進めたのと、翻訳ファイルの構造化など、今後運用していく中でも知見が溜まれば続編を書きたいと思います。

Discussion