next.jsのドキュメントには多言語対応のページがあります。
これを参考にすると、LPやホームページ程度であれば十分に多言語対応することができます。
next.jsだけでも出来ること
- 文字列を翻訳する
- アクセス時のaccept-languageヘッダーを見て自動的に言語を振り分ける (middleware.js)
- SSGで英語版、日本語版ページそれぞれ生成する (generateStaticParams())
あとは途中にコンポーネントが挟まるパターンに対応できれば、簡易的な翻訳としては十分です。
足りないもの: 途中にコンポーネントが挟まる翻訳
途中にコンポーネントが挟まるようなコンポーネントは、このようなコンポーネントです。
// 日本語版 <span><a href="http://example.com">解説</a>を読んでください</span>
//英語版 <span>read <a href="http://example.com">guide page</a></span>
日本語と英語はたいてい語順が異なるため(上の例でもa要素の位置が異なる)、コンポーネントが挟まった翻訳は難しいです。
これを解決するために、RichText というコンポーネントを導入してやります。
function RichText({ children, componentMap, }: { children: string; componentMap: Record<string, (children: ReactNode) => JSX.Element>; }): JSX.Element { const result: ReactNode[] = []; // [plainText, taggedText, plainText, taggedText, ...] という感じにパースして、taggedTextはコンポーネントで置き換える (["foo", <Link>link</Link> "bar"]みたいな) const regex = /<(\w+)>(.*?)<\/\1>/g; let lastIndex = 0, match; while ((match = regex.exec(children)) !== null) { const [fullMatch, tag, innerText] = match; const { index } = match; // plainTextを追加 result.push(children.slice(lastIndex, index)); // タグの中身を対応コンポーネントで包む。なければそのままテキストとして表示する result.push(componentMap[tag]?.(innerText) ?? fullMatch); lastIndex = index + fullMatch.length; } // 残りのplainTextを追加 result.push(children.slice(lastIndex)); return <>{result.filter((x) => typeof x !== 'string' || x.length > 0)}</>; }
これを使うと上のコンポーネントはこう書けます
const Page = ({params}: params: Promise<{ lang: string }>}) => { const dict = await getDictionary((await params).lang); return ( <span> <RichText componentMap={{ link: (children) => <a href="http://example.com">{children}</a>, }} > {dict['<link>解説</link>を読んでください']} </RichText> </span> ); };
まとめ: 簡易的な対応ならライブラリなしでできる
このように、RichText さえ追加してやれば ライブラリなしでnext.jsだけでも基本的な翻訳対応をできます。 ホームページやLPくらいであれば十分です。簡易的なアプリも対応できるでしょう。
もちろん、もっと本格的に対応しようと思ったらライブラリを入れたほうがよいです(日時、通貨、複数形、数値の区切りの対応などなど)。しかし、案外next.jsだけでもできるんだなということは覚えておくと役に立つかもしれません。
最後に、本記事をもとに実際に簡易的なi18nを実装したサンプルリポジトリを置いておきます。

