LoginSignup
3
2

Next.js で i18n

Last updated at Posted at 2023-07-14

i18n とは

ここでは、アプリケーションの国際化対応を行うことを言う。
国際化対応としては、言語を国や地域に応じて表示を出し分けるものがまずは思いつくが、本記事でもそれを対象としている。
一般的には、そのほかにも日時フォーマットやテキストの方向、測定単位なども i18n の対象範囲として挙げられるようだ。1
以下では、「i18n=多言語対応」として、Next.js アプリケーションの i18n を行うための進め方についてお話させていただく。

前提

  • react: 18.2.0
  • next: 12.1.6

前準備

Next.js で i18n を実現するためには、いくつかの設定値の変更や追加の設定コードが必要になる。
順を追って紹介する。

必要パッケージのインストール

npm install next-i18next
...
npm list next-i18next
#next-i18next@13.2.2

i18n の基本設定

next-i18next.config.js というファイルを作成し

touch next-i18next.config.js

以下のような記述をする。

next-i18next.config.js
/** @type {import('next-i18next').UserConfig} */
module.exports = {
  i18n: {
    defaultLocale: 'ja',
    locales: ['ja', 'en']
  }
};

この設定では、デフォルトの言語設定(以下ロケール)を ja(日本語)としている。
また、選択可能なロケールとして、ja 及び en(英語)を設定している。

次に、Next.js の設定ファイルに上の設定値を読み込むように以下の追記する。

next.config.js
const { i18n } = require('./next-i18next.config')

module.exports = {
  i18n,
}

ちなみに、今回は i18n 設定用ファイルを新規に作成したが、ファイルを分けずにこれらの内容を直接 next.config.js に入れ込んでも問題ない模様。

_app.tsx の修正

あまり深く考えずに HOC で囲むらしい。

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

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

export default appWithTranslation(MyApp)

前準備は以上となる。
以降では、翻訳テキストを表示する部分の処理を書いていく。

翻訳

以下のようなディレクトリ構成を想定する。(関連箇所のみ抜粋)

src/
├── components
│   └── Hello.tsx
└── pages
    ├── _app.tsx
    └── hello.tsx

基本編

翻訳テキストの用意

翻訳対象としたいテキストデータは、デフォルトでは、/public/locales/{locale}/** に格納する必要がある。
また、データは json 形式となる。
今回は ja および en のロケール選択を可能にしているので、それぞれのディレクトリを作成する。

mkdir public/locales
mkdir public/locales/ja
mkdir public/locales/en

翻訳テキストデータの格納場所は、next-i18next.config.js の設定オプション localePath で変更可能である。

次に、各ロケールのディレクトリ配下に、common.json と言うファイルを作成する。

touch public/locales/ja/common.json
touch public/locales/en/common.json

作成したファイルに、key:value の形式で、翻訳テキストを書く。
今回は、日本語をキーとして、ロケールが ja の場合は、そのまま日本語を、en の場合は英語の翻訳が表示されるようにしている。

ja/common.json
{
  "こんにちは": "こんにちは"
}
en/common.json
{
  "こんにちは": "Hello"
}

common は、デフォルトのネームスペース名となる。
next-i18next.config.js の設定オプション defaultNS で変更可能である。
また、ネームスペースについては後述する

テキストを表示させる

useTranslation というフックを使用して、以下のようなコードを書く。

src/components/Hello.tsx
import { useTranslation } from 'next-i18next';
import React from 'react';

export const Hello = () => {
  const { t } = useTranslation();
  return <p>{t('こんにちは')}</p>;
};
src/pages/index.tsx
import React from 'react';
import { Hello } from '../../components/Hello';

const Index = () => {
  return <Hello />;
};
export default Index;

next dev すると、画面には、「こんにちは」と表示された。

ロケールの切り替え

このままでは日本語から離れられないので、ロケールの切り替えを考える。
以下の追記を行う。

src/pages/index.tsx
+ import { GetServerSideProps } from 'next';
+ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { Hello } from '../../components/Hello';

const Index = () => {
  return <Hello />;
};
export default Index;

+ export const getServerSideProps: GetServerSideProps = async (context) => {
+   const locale = 'en';
+   return { props: { ...(await serverSideTranslations(locale)) } };
+ };

getServerSideProps は SSR の関数である。この中でロケールの指定(locale=en)を行い、それを serverSideTranslations に渡す。これにより、この pages/index.tsx 配下で呼ばれるコンポーネント内では、英語の翻訳が適用されるようになる。

実際に、画面を確認すると、今度は「Hello」と表示されているはずだ。

ネームスペースとは?

デフォルトネームスペースが common であると上述した。ネームスペースは、翻訳テキストを任意の粒度で管理することができる機能である(と個人的に理解している)。翻訳ファイル public/locales/<locale>/** 配下のディレクトリ構成がそのままネームスペースとなる。そして、翻訳テキストを参照するときにネームスペースを指定する必要があり、未指定の場合はデフォルトの値(common)が採用される。 上の例では、「こんにちは」と言う翻訳に関するデータは common ネームスペースに入れているので、ネームスペースの指定を行う必要がなかったのである。

他のネームスペースの利用

common 以外のネームスペース(foo)を利用してみよう。
以下のファイルを作成する。(ja の場合も同様のため、割愛)

touch public/locales/en/foo.json
public/locales/en/foo.json
{
  "ありがとう": "Thank you"
}

以下の追記を行う。

src/pages/index.tsx
import { GetServerSideProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { Hello } from '../../components/Hello';

const Index = () => {
  return <Hello />;
};
export default Index;

export const getServerSideProps: GetServerSideProps = async (context) => {
  const locale = 'en';
-  return { props: { ...(await serverSideTranslations(locale)) } };
+  return { props: { ...(await serverSideTranslations(locale, ['foo'])) } };
};

serverSideTranslations の第二引数にネームスペースの(配列で)与えた。

さらに、

src/components/Hello.tsx
import { useTranslation } from 'next-i18next';
import React from 'react';

export const Hello = () => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('foo');
  return <p>{t('ありがとう')}</p>;
};

こうすることで、デフォルトネームスペース以外の翻訳テキストについても表示できるようになった。

動的なロケールの変更

実際のアプリケーションでは、ロケールは動的に変更できるべきである。
Next.js では簡単に実現できるようになっている。
以下は、ロケールを切り替えるコンポーネントの一例である。
なお、Next.js のロケール処理の詳細はこちらを参照されたし。

components/LocaleSwitcher.tsx
import Link from 'next/link';
import { useRouter } from 'next/router';

export const LocaleSwitcher = ({ href }: { href: string }) => {
  const router = useRouter();
  const locale = getLocale(router.locale);
  const switchedLocale = locale === 'ja' ? 'en' : 'ja';

  return (
    <Link href={href} locale={switchedLocale}>
      {`言語を ${switchedLocale} に変更する`}
    </Link>
  );
};

翻訳テキストの側にロケール切り替え用コンポーネントを配置する。

src/pages/index.tsx
import { GetServerSideProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { Hello } from '../../components/Hello';
+ import { LocaleSwitcher } from '../components/LocaleSwitcher';

const Index = () => {
+  const router = useRouter();
  return (
    <>
+      <LanguageSwitcher href={router.asPath} />
      <Hello />
    </>
  );
};
export default Index;

export const getServerSideProps: GetServerSideProps = async (context) => {
-  const locale = 'en';
+  const locale = context.locale || 'ja';// context.locale が undefined の可能性があるため
  return { props: { ...(await serverSideTranslations(locale, ['foo'])) } };
};

リンクをクリックすることで、ja <-> en の切り替えが可能になった。
また、この時のページ URL はそれぞれ {HOST}/ および {HOST}/en となっている。

応用編

以上で大体の i18n の仕組みを説明した。
ここからは、少し複雑なパターンについて少し紹介する。

ネストされたネームスペースの利用

public/locales/{en,ja}/bar/baz.json にある翻訳テキストを対象とする場合を考える。
階層は、スラッシュによる区切りで表現する。

public/locales
├── en
│   ├── common.json
│   ├── foo.json
│   └── bar
│       └── baz.json
└── ja
    ├── common.json
    ├── foo.json
    └── bar
        └── baz.json
en/bar/baz.json
{
  "さようなら": "Good-bye"
}
ja/bar/baz.json
{
  "さようなら": "さようなら"
}

変更箇所のみ抜粋

src/pages/index.tsx
...

export const getServerSideProps: GetServerSideProps = async (context) => {
  const locale = context.locale || 'ja';
-  return { props: { ...(await serverSideTranslations(locale, ['foo'])) } };
+  return { props: { ...(await serverSideTranslations(locale, ['bar/baz'])) } };
};
src/components/Hello.tsx
...
export const Hello = () => {
-  const { t } = useTranslation('foo');
+  const { t } = useTranslation('bar/baz');
  return <p>{t('さようなら')}</p>;
};

複数ネームスペースの利用

serverSideTranslations および useTranslation にネームスペースの配列を渡す。
さらに、t()で翻訳キーを指定する際、useTranslation で配列の二番目以降に指定したネームスペースに属するキーに関しては、ns: <namespace> でネームスペースを指定する必要がある。

変更箇所のみ抜粋

src/pages/index.tsx
...

export const getServerSideProps: GetServerSideProps = async (context) => {
  const locale = context.locale || 'ja';
-  return { props: { ...(await serverSideTranslations(locale, ['bar/baz'])) } };
+  return { props: { ...(await serverSideTranslations(locale, ['foo', 'bar/baz'])) } };
};
src/components/Hello.tsx
...
export const Hello = () => {
-  const { t } = useTranslation('foo');
+  const { t } = useTranslation(['foo', 'bar/baz']);
  return (
    <p>
-      {t('さようなら')}
+      {t('ありがとう')}/{t('さようなら', { ns: 'bar/baz' })}
    </p>
  );
};

この例では、階層立ったネームスペースが二番目以降に指定されるという合わせ技になっている。

おまけ

このような状態で common の翻訳テキスト扱いはどうなるかだが、serverSideTranslations では指定が必要であり、useTranslation では指定が不要である。そして、t()で翻訳キーを指定するときに ns: 'common' をそっと添えてあげる。

src/components/Hello.tsx
...
export const Hello = () => {
  const { t } = useTranslation(['foo', 'bar/baz']);
  return (
    <p>
-      {t('ありがとう')}/{t('さようなら', { ns: 'bar/baz' })}
+      {t('ありがとう')}/{t('さようなら', { ns: 'bar/baz' })}/{t('こんにちは', { ns: 'common' })}
    </p>
  );
};

Dockerfile への記述

Docker イメージにする場合は、config ファイル2つを WORKDIR に追加する必要がある。

Dockerfile
FROM registry.access.redhat.com/ubi8/nodejs-16-minimal:1
WORKDIR /opt/app-root/src
...
+ COPY next-i18next.config.js /opt/app-root/src/
+ COPY next.config.js /opt/app-root/src/
...
CMD ["npm", "start"]

参考

  1. https://developer.mozilla.org/ja/docs/Glossary/I18N

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2