logoChibiham
cover
🌐

Next.js App router で多言語化対応 w/next-i18n-router

next-i18n-routeri18nextを使用して、すっきりとi18nに対応します。

加えて、react-i18nextによってCSRにも対応します。

サンプルをGitHubで公開しています。

next-i18n-routerの設定

まずはnext-i18n-routerのREADMEに書いてある通りです(ほぼ英訳)。

  • package install
      bash
      	npm install next-i18n-router
  • configを作る
      typescript
      export const i18nConfig = {
        locales: ["en", "ja"],
        defaultLocale: "ja",
      };
      /i18n/config.ts
  • app下のファイルをすべて[locale]下に移動する
      bash
      	└── app
          └── [locale]
              ├── layout.js
              └── page.js
  • middlewareを追加する
      bash
      import { i18nRouter } from "next-i18n-router";
      import { NextRequest } from "next/server";
      import { i18nConfig } from "@/i18n/config";
      
      export function middleware(request: NextRequest) {
        return i18nRouter(request, i18nConfig);
      }
      
      // only applies this middleware to files in the app directory
      export const config = {
        matcher: "/((?!api|static|.*\\..*|_next).*)",
      };
  • ここまでの設定によって、クライアントサイド/サーバーサードそれぞれ以下のような形で、ブラウザの言語設定にしたがったロケールを取得できるようになります。

    typescript
    'use client';
    
    import { useCurrentLocale } from 'next-i18n-router/client';
    import i18nConfig from '@/i18nConfig';
    
    function ExampleClientComponent() {
      const locale = useCurrentLocale(i18nConfig);
    
      ...
    }
    typescript
    	// server component
    function ExampleServerComponent({ params: { locale } }) {
      ...
    }

    そして、default localeをjaにしている場合、ブラウザの設定が日本語の場合はlocaleのパスが省略されます。たとえばproducts へのルーティングの場合、パスは

    日本語: /products

    英語: /en/products

    となります。

    i18nextによるSserver Componentの対応

    i18nextを導入してSSGされるコンポーネントに対応します。

  • package install
      typescript
      npm install i18next
  • i18nextの言語設定を追加します。
      typescript
      import { InitOptions } from "i18next";
      import { en } from "./en";
      import { ja } from "./ja";;
      
      export const i18nextInitOptions: InitOptions = {
        lng: "ja",
        fallbackLng: "ja",
        resources: {
          en,
          ja,
        },
      };
      i18n/config
      typescript
      export const ja = {
        translation: {
          hello: "こんにちは!",
        },
      };
      i18n/ja.ts
      typescript
      export const en = {
        translation: {
          hello: "Hello!",
        },
      };
      i18n/en.ts
  • 設定した言語設定を使用し、layout.tsxでi18nインスタンスの初期化および言語設定を行います。
      typescript
      import i18n from "i18next";
      import { i18nextInitOptions } from "@/i18n/config";
      
      i18n.init(i18nextInitOptions, (err) => {
        if (err) {
          console.error("i18next failed to initialize", err);
        }
      });
      
      export default function RootLayout({
        children,
        params: { locale },
      }: {
        children: React.ReactNode;
        params: {
          locale: string;
        };
      }) {
        i18n.changeLanguage(locale);
        return (
          <html lang={locale}>
            <body>
              {children}
            </body>
          </html>
        );
      }
  • これで、Server Componentでクライアントの言語設定にしたがったt関数が使用できます。
      typescript
      import { t } from "i18next";
      
      export default function Hello() {
        return <p>{t("hello")}</p>; // 日本語なら`こんにちは!`、 英語なら`Hello!`
      }
  • react-i18nextによるClient Componentの対応

    i18nextの設定ファイルはそのままに、csrも同様にt関数をできるようにします。

  • package install
      typescript
      npm install react-i18next
  • Providerを用意します。layout.tsxに追加しますが、I18nextProviderはserver componentなのでこちらはclient componentとして用意する必要があります。(use clientが必要、ということです)
      typescript
      "use client";
      import i18n from "i18next";
      import { i18nConfig, i18nextInitOptions } from "@/i18n/config";
      import { I18nextProvider } from "react-i18next";
      import { useCurrentLocale } from "next-i18n-router/client";
      import { ReactNode } from "react";
      
      i18n.init(i18nextInitOptions, (err) => {
        if (err) {
          console.error("i18next failed to initialize", err);
        }
      });
      
      export const I18nProvider = ({ children }: { children: ReactNode }) => {
        i18n.changeLanguage(useCurrentLocale(i18nConfig));
        return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
      };
  • layout.tsxに追加します。
      typescript
      import i18n from "i18next";
      import { i18nextInitOptions } from "@/i18n/config";
      import { I18nProvider } from "./i18nProvider";
      
      export const metadata = {
        title: "Next.js",
        description: "Generated by Next.js",
      };
      
      i18n.init(i18nextInitOptions, (err) => {
        if (err) {
          console.error("i18next failed to initialize", err);
        }
      });
      
      export default function RootLayout({
        children,
        params: { locale },
      }: {
        children: React.ReactNode;
        params: {
          locale: string;
        };
      }) {
        i18n.changeLanguage(locale);
        return (
          <html lang={locale}>
            <body>
              <I18nProvider>{children}</I18nProvider>
            </body>
          </html>
        );
      }
  • これで、Server Componentと同様にClient Componentでも同じように多言語対応できます。

    typescript
    "use client";
    import { t } from "i18next";
    
    export default function Hello() {
      return <p>{t("hello")}</p>; // 日本語なら`こんにちは!`、 英語なら`Hello!`
    }

    所感

    はじめはNext.jsのドキュメントにある形で実装しましたがちょっと煩雑になっている感が否めなかったのですが、next-i18n-routerにあやかりスッキリしました。

    Server Componentsでの対応はNextRequestでi18nのインスタンス管理ができればmiddlewareで設定できそうな気もしますが、上手くできなかったのでlayout.tsxに記述してしまう形で落ち着きました。(いい方法あればぜひコメントください)