Next.jsをi18nに対応させる

Next.jsで多言語化対応のためにやったことをまとめます。
まず、こちらのパッケージを使用していきます。
next-i18next

検証

$ git clone https://github.com/isaachinman/next-i18next.git
$ cd next-i18next/examples/simple

リポジトリ内にサンプルコードも上がっているのでそちらを参考にしつつ進めます。
なお、サンプルコードはyarnで初回インストールしてもパッケージが足りないと怒られるのでpackage.jsonのdependenciesを以下のように書き換えてください。

  "dependencies": {
    "express": "^4.16.4",
    "i18next": "^14.0.1",
    "next": "^9.1.6",
    "next-i18next": "^4.4.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }
$ yarn
$ yarn dev

動作確認をして問題ないことが確認できたら、サンプルコードを参照して書いていきます。
READMEの方に書いてもありますが、若干動作しないところも見受けられたので、サンプルコードから引用して確実にいきます。

インストール

ReactとNext.js両方に対応していてNext.jsでやる場合はカスタムサーバー構成でやるように書かれています。なのでSSRを想定した実装になります。

$ yarn add i18next next-i18next

翻訳ファイルのフォルダ構成

static
  └── locales
      ├── en
      |   └── common.json
      └── ja
          └── common.json

common.jsはこんな感じで準備します。
/ja/common.json

{
  "h1": "サンプル"
}

/en/common.json

{
  "h1": "A simple example"
}

next.config.jsにpublicRuntimeConfigを追加して以下のように設定

module.exports = {
  publicRuntimeConfig: {
    localeSubpaths: typeof process.env.LOCALE_SUBPATHS === 'string'
      ? process.env.LOCALE_SUBPATHS
      : 'none',
  },
}

server.js

const express = require('express');
const next = require('next');
const nextI18NextMiddleware = require('next-i18next/middleware').default;

const nextI18next = require('./i18n');

const port = process.env.PORT || 3000;
const app = next({ dev: process.env.NODE_ENV !== 'production' })
const handle = app.getRequestHandler();

(async () => {
  await app.prepare()
  const server = express()

  await nextI18next.initPromise;
  server.use(nextI18NextMiddleware(nextI18next))

  server.get('*', (req, res) => handle(req, res))

  await server.listen(port)
  console.log(`> Ready on http://localhost:${port}`) // eslint-disable-line no-console
})()

server.jsにミドルウェアをかましてあげる。

ルートにindex.jsを作成

const { setConfig } = require('next/config');
setConfig(require('./next.config'));

require('./server');

ここはNext.jsに存在していない部分なので作る必要がある。何をしているのかというと、next.config.jsを読み込んだ上でserver.jsを実行する必要があるため、このindex.jsは繋ぐ役割をしています。

ルートにi18n.jsの作成

const NextI18Next = require('next-i18next').default;
const { localeSubpaths } = require('next/config').default().publicRuntimeConfig;

const localeSubpathVariations = {
  none: {},
  foreign: {
    ja: 'ja'
  },
  all: {
    ja: 'ja',
    en: 'en',
  },
}

module.exports = new NextI18Next({
  otherLanguages: ['ja', 'en'],
  localeSubpaths: localeSubpathVariations[localeSubpaths]
});

サンプルにはコピーしないでって書かれてるので、サンプルをベースに編集、英語と日本語対応。

_app.js

import React from 'react';
import App from 'next/app';
import { appWithTranslation } from '../i18n';

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props
    return (
      <Component {...pageProps} />
    );
  };
};

export default appWithTranslation(MyApp);

appをHOCで挟みます
これでpropsからtransrate用のAPIが渡ってくるようになります。

/page/index.tsx

import React from 'react';
import { NextPage } from 'next';
import Layout from '../layouts/Layout';
import { i18n, withTranslation } from '../i18n';

const Index: NextPage = ({ t }): JSX.Element => {
  console.log(i18n.language); // 現在選択中の言語
  return (
    <Layout>
      <div>{t('h1')}</div>
      <button
          type='button'
          onClick={() => i18n.changeLanguage(i18n.language === 'en' ? 'ja' : 'en')}
        >
          {'切り替え'}
        </button>
    </Layout>
  );
};

export default withTranslation('common')(Index);

ルートから実行

$ node index.js

動作確認して問題なければ言語切り替え完了
tsで書いていて型の確認を行いたい場合は、こちら

Tips

Next.js latestでパッケージを指定すると怒られるのでvarsion 9代にする必要があります。

サーバーレスに対応

先ほどはSSRを想定した実装でしたが、CSRやSSGなどのサーバーレスに対してはどのようにアプローチしていくか?カスタムサーバーにミドルウェアを挟まずに実装する方法については、こちらが参考になります。
i18n for Serverless Next.js app
react-i18next documentation – Quick start

個人的な所感ですが、後者のReactを対象とした実装方法の方が設定も少なく済むのでよいのではないかと思います。また、上記の場合はHOCで書くやり方でしたが、react-i18nextではHooksが使用できるため比較的わかりやすいコードが書けます。

$ yarn add react-i18next i18next --save

i18n.js

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

// the translations
// (tip move them in a JSON file and import them)
const resources = {
  en: {
    translation: {
      "Welcome": "Hello"
    }
  },
  ja: {
    translation: {
      "Welcome": "こんにちわ!"
    }
  }
};

i18n
  .use(initReactI18next) // passes i18n down to react-i18next
  .init({
    resources,
    lng: "ja", // default language
    keySeparator: false, // we do not use keys in form messages.welcome
    interpolation: {
      escapeValue: false // react already safes from xss
    }
  });

export default i18n;

app.jsでも読み込んであげる

import React from 'react';
import App from 'next/app';
import '../i18n';

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props;
    return (
      <>
          <Component {...pageProps} />
      </>
    );
  }
}

export default MyApp;

useTranslationを呼び出して、該当箇所に使ってみる

import React from 'react';
import { useTranslation } from 'react-i18next';

const Hoge: React.FunctionComponent = (): JSX.Element => {
  const { i18n, t } = useTranslation();
  return (
    <div>
      <div onClick={() => i18n.changeLanguage(i18n.language === 'en' ? 'ja' : 'en')}>{t('Welcome')}</div>
    </div>
  );
};
export default Hoge;

まとめ

Reactでi18n対応は思ったよりもすぐに実装できました。多言語化対応の要望も多々あるので、今後とも活躍してくれそうです。

関連記事