結論
設計思想にこだわりがないのならApp Router + SSR のi18n対応はnext-intl が楽だと思います。
zod
を使ってバリデーションもi18n対応場合は next-i18next
のほうが親和性は高いですが、工夫次第で next-intl
でも問題なし。
Next.jsでi18nするぞーとなってググったとき、最初に見つかったのが next-i18next
でした。このときは next-intl
の存在すら知らずreact-i18next
と何が違うかわからんけどnextだしいっか、と使い始め、結果良くできてたライブラリで安心してました。
ここまではいいんです。問題はApp Routerです。
App Router with SSR
next-i18next自体はSSRでのレンダリングに対応しているらしいのですが、素人TypeScriptエンジニアである私からすると、設定方法がよくわからず大変そうだなという印象でした。(そもそもSSRとCSR/SSGの根本の違いを理解していない前提もあり)
結果的にプロエンジニアの力を借りて実装そのものはできたのですが、自分が理解するまでに骨が折れそうだなと今は思っています。
そして実際骨が折れつつもこの課題について素晴らしくまとめてある記事もあります。
代替手段 next-intl
これからi18n対応するという前提で、もう少し楽にi18nする方法はないのかなと改めて検索すると、以下のWebサイトが引っかかりました。
先程のZennの記事と似てるのですが、こちらは多言語ファイルをWebで管理できる i18nexus というツールのチュートリアル記事のようでした。
ここに見覚えのない単語があります。
それが 「next-intl」 Who Are You?
Githubのスター数は現時点(2024/12)で3Kほど next-i18next
のほうが上ですが、Googleトレンドではすでに next-intl
のほうが上です。
おそらく導入時、SSG/SSRによって実装手順に大きな違いがないという容易性がウケてるんだと思います。(少なくとも私は簡単だなと感じました)
これはi18nexusのチュートリアルをみても自明だと思います。
SSRでさえ以下のように宣言するだけで、SSGと同じように非同期翻訳が使えます。
const t = await getTranslations("Week");
参考:
Zodのバリデーションとi18n対応
別の観点としてよく使うユースケースだと思われる Zod
のバリデーションを多言語化するという例で next-i18next
と next-intl
を比較してみます。
Zodのi18nは以下で対応されています。
exampleにあるように、 next-i18next
での使い方は以下のような感じです。
const { t } = useTranslation();
z.setErrorMap(makeZodI18nMap({ t }));
が、これをそのままApp Routerで使うことはできません。先述どおりSSR用の useTranslation
を自分で実装・定義する必要があります。
next-intl
はどうでしょうか。
こちらは標準で zod-i18n
には対応していないため、以下のブログのように組み込み方を少し工夫する必要があります。
どちらも一長一短ありますが、個人的には next-intl
だと 項目名のi18nも簡単そうにみえますね。
例えば入力欄ではなく、画面上部にエラーメッセージをまとめて表示するというユースケースのとき、「username は必須です」と出力されるよりも「ユーザー名は必須です」と出てほしいっていうやつです。 ここでの username
は以下のようなZodのスキーマを前提としたときのキー名です。
const schema = z.object({
username: z.string().min(3),
age: z.number().min(18),
});
まとめ
Next.jsでのi18n対応に1つ選択肢が増えたので、プロダクトの方向性やエンジニアチームの趣味趣向、アーキテクチャの具合や今後を踏まえてどちらかを選択すると良いかと思います。
色々面倒なことを省き、これからのエコシステムの発展に期待するという側面では、 next-intl
が有利かなと個人的には思います。