- 2023-06-04 18:00: 誤りのある表現を修正しました
s/Static Generation/Static Exports/
*1
結論
Next.js の i18n 機能は Static Exports 未サポートなので自前で頑張れ!
Next.js App Router の Static Exports で i18n したい!
今回、以下のような要件を決めて、実現できるかチャレンジしました:
- Next.js の App Router を用いる
- 言語は日本語(ja)と英語(en)の 2種類
- サブパスで言語を分けるが、デフォルト言語のサブパスは切りたくない
- 日本語ページのパスが
/hogehoge/
なら、英語ページは/en/hogehoge/
という感じにしたい
- 日本語ページのパスが
output: 'export'
モードにし、Static Exports する- 生成された各ページの HTML に、localize された後のテキストがそのまま埋め込まれている
ゴリ押しは全てを解決する
試行錯誤した結果、以下のコードのようになりました。
なぜゴリ押ししたかというと、Next.js の i18n 機能は Static Exports をサポートしていないからです。
コード解説
Server Component での i18n の実現
以下の記事を読んで大体同じようにコードを書きました。ここに書かれている通りにカスタムフックを作ると、App Router を用いた Server Componnent を i18n することができます。
import { createInstance } from 'i18next' import resourcesToBackend from 'i18next-resources-to-backend' import { initReactI18next } from 'react-i18next/initReactI18next' export const defaultLanguage = 'ja' export const nonDefaultLanguages = ['en'] as const export const languages = [defaultLanguage, ...nonDefaultLanguages] as const export type Language = (typeof languages)[number] type useTranslationProps = Readonly<{ lang: string translationDef: Readonly<{ [L in Language]: Readonly<{}> }> }> export const useTranslation = async ({ lang, translationDef }: useTranslationProps) => { const i18n = createInstance() await i18n .use(initReactI18next) .use(resourcesToBackend(translationDef)) .init({ debug: process.env.NODE_ENV === 'development', supportedLngs: languages, fallbackLng: defaultLanguage, lng: lang, }) return { t: i18n.getFixedT(lang), i18n, } }
デフォルト言語だけサブパスを切らないルーティングの実現
app ディレクトリ以下を以下のような構成にしました
残念ながらこうするしかないようです。[lang] は動的ルーディングで、ここに en が入るというイメージ。(root)/ 以下の方が日本語のページに対応します。
ポイントは (root)/ というディレクトリ。こいつを app/ 直下に配置すると、[lang]/ 以下のページを閲覧したときに両方の layout が使われマトリョーシカになってしまいます。App Router には route group という仕組みがあり、()
で囲まれたディレクトリを用いると、URL のパスに影響を与えることなく component を分類することができます。
(root)/ と [lang]/ 以下に同じようなコードを書くのは馬鹿らしいので、(root)/ 以下のファイルは [lang]/ 以下で定義された component を import し default の言語を props で渡してあげるようにしています。
app/(root)/page.tsx
import IndexPage from '../[lang]/page' import { defaultLanguage } from '../_hooks/i18n' export default async function DefaultIndexPage() { return await IndexPage({ params: { lang: defaultLanguage, }, }) }
app/[lang]/page.tsx
import { nonDefaultLanguages, useTranslation } from '../_hooks/i18n' import translationDef from './translationDef' export const generateStaticParams = async () => nonDefaultLanguages.map(lang => ({ lang })) type IndexPageProps = Readonly<{ params: { lang: string } }> export default async function IndexPage({ params }: IndexPageProps) { const { lang } = params const { t } = await useTranslation({ lang, translationDef }) return ( <main> <h1>{t('greeting')}</h1> </main> ) }
(root) 以下のファイルをコード生成するツールを自作すると捗りそうですね。
言語の切り替えリンク
以下のようなコードで、言語だけ切り替えられるようなリンクを作りました。hogehoge/
ページにいる場合は en/hogehoge/
に、 en/hogehoge/
ページにいる場合は hogehoge/
にリンクする、といった挙動です。
'use client' import Link from 'next/link' import { usePathname } from 'next/navigation' import { nonDefaultLanguages } from '@/app/_hooks/i18n' type ToggleLanguageLinkProps = Readonly<{ children: React.ReactNode }> export default function ToggleLanguageLink({ children }: ToggleLanguageLinkProps) { const pathname = usePathname() const enPrefix = `/${nonDefaultLanguages[0]}` let newPathname: string if (pathname.startsWith(enPrefix)) { newPathname = pathname.substring(enPrefix.length) } else { newPathname = `${enPrefix}${pathname}` } return <Link href={newPathname}>{children}</Link> }
ページ内容を維持するために現在のパスを取得する必要があり、そのためのフック usePathname() が Next.js で用意されています。しかし、これは Server Component では利用できません。'use client'
と書くことにより Client Component にしています。
Client Component だと Static Exports したときに HTML として書き出せないのではないか?という心配は無用です。
With Static Rendering, both Server and Client Components can be prerendered on the server at build time.
https://nextjs.org/docs/app/building-your-application/rendering#static-rendering
感想
いかがでしたか?
Gatsby なら gatsby-plugin-react-i18next plugin を入れて以下のように config を書くだけで実現できるのですが、Next.js の Static Exports ではこううまくはいかないのが現実だなあと感じさせられました。
{ resolve: `gatsby-plugin-react-i18next`, options: { localeJsonSourceName: `locale`, languages: [`ja`, `en`], defaultLanguage: `ja`, siteUrl, } },
まあ Vercel 使ってねってことなんでしょうけど。i18n 要件のある静的サイトを作るのに Next.js を選択するのはまだまだ厳しいなと思いました。良いやり方知っている人いたらぜひ教えてください。
そして全く同じような境遇の人のエントリを発見しました……