🍳
Next.js App Router (app ディレクトリ)使った多言語対応
Next.js 13 の App Router(appディレクトリ)を使用した多言語対応に関する記事です。こちらの記事を参考に構築しました。
前提条件
- Next.jsアプリ作成済み
- App Router(appディレクトリ)を使用する
- JavaScript(TypeScriptは使わない)
- 日本語と英語に対応する
構築していく
パッケージをインストール
多言語対応に必要なパッケージインストールをします(npmの場合)
npm install i18next react-i18next i18next-resources-to-backend accept-language
フォルダ構成
言語を URL パラメーターとして使います。動的ルーティングに関する不明点はDynamic Routesに関するドキュメントを読みます。
.
└── app
└── [lng]
├── about
| └── page.js
├── layout.js
└── page.js
└── i18n
└── locales
├── ja
| ├── home.json
| └── about.json
└── en
├── home.json
└── about.json
└── index.js
└── settings.js
└── components
└── Footer
└── index.js
ホームページ
app/[lng]/page.js
import Link from 'next/link'
export default function Page({ params: { lng } }) {
return (
<>
<h1>ホームページです</h1>
<Link href={`/${lng}/about`}>
About page
</Link>
</>
)
}
Aboutページ
app/[lng]/about/page.js
import Link from 'next/link'
export default function Page({ params: { lng } }) {
return (
<>
<h1>Aboutページです</h1>
<Link href={`/${lng}`}>
back
</Link>
</>
)
}
layout.js
app/[lng]/layout.js
import { dir } from 'i18next'
const languages = ['ja', 'en']
export async function generateStaticParams() {
return languages.map((lng) => ({ lng }))
}
export default function RootLayout({
children,
params: {
lng
}
}) {
return (
<html lang={lng} dir={dir(lng)}>
<head />
<body>
{children}
</body>
</html>
)
}
言語の検出
cookie に言語設定を保存して、設定言語がどのページにも反映されるようにします。
i18nフォルダ内にsettings.js
を作成し、layout.js
を編集します。
app/i18n/settings.js
export const fallbackLng = 'ja'
export const languages = [fallbackLng, 'en']
app/[lng]/layout.js
import { dir } from 'i18next'
+ import { languages } from '../i18n/settings'
export async function generateStaticParams() {
return languages.map((lng) => ({ lng }))
}
export default function RootLayout({
children,
params: {
lng
}
}) {
return (
<html lang={lng} dir={dir(lng)}>
<head />
<body>
{children}
</body>
</html>
)
}
middleware.js
を作成します。
middleware.js
import { NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { fallbackLng, languages } from './app/i18n/settings'
acceptLanguage.languages(languages)
export const config = {
matcher: '/:lng*'
}
const cookieName = 'i18next'
export function middleware(req) {
let lng
if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value)
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
if (!lng) lng = fallbackLng
if (req.nextUrl.pathname === '/') {
return NextResponse.redirect(new URL(`/${lng}`, req.url))
}
if (req.headers.has('referer')) {
const refererUrl = new URL(req.headers.get('referer'))
const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
const response = NextResponse.next()
if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
return response
}
return NextResponse.next()
}
i18n
i18nフォルダ内にindex.js
を作成します。
app/i18n/index.js
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { getOptions } from './settings'
const initI18next = async (lng, ns) => {
const i18nInstance = createInstance()
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
.init(getOptions(lng, ns))
return i18nInstance
}
export async function useTranslation(lng, ns, options = {}) {
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options.keyPrefix),
i18n: i18nextInstance
}
}
app/i18n/settings.js
にオプションを追加します。
app/i18n/settings.js
export const fallbackLng = 'ja'
export const languages = [fallbackLng, 'en']
+ export const defaultNS = 'home'
+ export function getOptions (lng = fallbackLng, ns = defaultNS) {
+ return {
+ supportedLngs: languages,
+ fallbackLng,
+ lng,
+ fallbackNS: defaultNS,
+ defaultNS,
+ ns
+ }
+ }
翻訳ファイルの用意
ホームページ、Aboutページそれぞれの翻訳ファイルを用意します。
defaultNS = 'home'
と設定しているので、デフォルトではhome.jsonが呼ばれます。
ホームページ
app/i18n/locales/ja/home.json
{
"title": "ホームページです",
"to-about-page": "Aboutページへ"
}
app/i18n/locales/en/home.json
{
"title": "HomePage",
"to-about-page": "To about page"
}
Aboutページ
app/i18n/locales/ja/about.json
{
"title": "Aboutページです",
"back": "もどる"
}
app/i18n/locales/en/about.json
{
"title": "AboutPage",
"back": "back"
}
各ページに反映させる
app/[lng]/page.js
import Link from 'next/link'
import { useTranslation } from '../i18n'
export default async function Page({ params: { lng } }) {
+ const { t } = await useTranslation(lng)
return (
<>
<h1>{t('title')}</h1>
<Link href={`/${lng}/about`}>
{t('to-about-page')}
</Link>
</>
)
}
app/[lng]/about/about.js
import Link from 'next/link'
import { useTranslation } from '../../i18n'
export default async function Page({ params: { lng } }) {
//デフォルトの翻訳ファイル以外を呼ぶときは追記する。今回は'about'
+ const { t } = await useTranslation(lng, 'about')
return (
<>
<h1>{t('title')}</h1>
<Link href={`/${lng}`}>
{t('back')}
</Link>
</>
)
}
言語切り替えボタンの作成
コンポーネント作成
Footer
コンポーネントを作成します。この中に言語切替するための部品を作ります。
app/[lng]/components/Footer/index.js
import Link from 'next/link'
import { Trans } from 'react-i18next/TransWithoutContext'
import { languages } from '../../../i18n/settings'
import { useTranslation } from '../../../i18n'
export const Footer = async ({ lng }) => {
const { t } = await useTranslation(lng, 'footer')
return (
<footer style={{ marginTop: 50 }}>
<Trans i18nKey="languageSwitcher" t={t}>
Switch from <strong>{{lng}}</strong> to:{' '}
</Trans>
{languages.filter((l) => lng !== l).map((l, index) => {
return (
<span key={l}>
{index > 0 && (' or ')}
<Link href={`/${l}`}>
{l}
</Link>
</span>
)
})}
</footer>
)
}
Footerコンポーネント用の翻訳ファイルを日英用意
app/i18n/locales/ja/footer.json
{
"languageSwitcher": "言語切替<1>{{lng}}</1>: "
}
app/i18n/locales/en/footer.json
{
"languageSwitcher": "Switch from <1>{{lng}}</1> to: "
}
各ページにFooterコンポーネントを追加
app/[lng]/page.js
import Link from 'next/link'
import { useTranslation } from '../i18n'
+ import { Footer } from './components/Footer'
export default async function Page({ params: { lng } }) {
const { t } = await useTranslation(lng)
return (
<>
<h1>{t('title')}</h1>
<Link href={`/${lng}/about`}>
{t('to-about-page')}
</Link>
+ <Footer lng={lng}/>
</>
)
}
app/[lng]/about/about.js
import Link from 'next/link'
import { useTranslation } from '../../i18n'
+ import { Footer } from '../components/Footer'
export default async function Page({ params: { lng } }) {
const { t } = await useTranslation(lng, 'about')
return (
<>
<h1>{t('title')}</h1>
<Link href={`/${lng}`}>
{t('back')}
</Link>
+ <Footer lng={lng}/>
</>
)
}
パスの設定の追加
今のままだと、ホームページの方は問題なく動作するはずです。
一方、Aboutページは言語切り替えするとホームページに遷移してしまうので、パスの設定の追加をします。
app/[lng]/components/Footer/index.js
import Link from 'next/link'
import { Trans } from 'react-i18next/TransWithoutContext'
import { languages } from '../../../i18n/settings'
import { useTranslation } from '../../../i18n'
export const Footer = async ({ lng }) => {
const { t } = await useTranslation(lng, 'footer')
return (
<footer style={{ marginTop: 50 }}>
<Trans i18nKey="languageSwitcher" t={t}>
Switch from <strong>{{lng}}</strong> to:{' '}
</Trans>
{languages.filter((l) => lng !== l).map((l, index) => {
return (
<span key={l}>
{index > 0 && (' or ')}
- <Link href={`/${l}`}>
- {l}
- </Link>
+ <Link href={`/${l}${path}`}>{l}</Link>
</span>
)
})}
</footer>
)
}
app/[lng]/about/about.js
import Link from 'next/link'
import { useTranslation } from '../../i18n'
import { Footer } from '../components/Footer'
export default async function Page({ params: { lng } }) {
const { t } = await useTranslation(lng, 'about')
return (
<>
<h1>{t('title')}</h1>
<Link href={`/${lng}`}>
{t('back')}
</Link>
- <Footer lng={lng} />
+ <Footer lng={lng} path="/about"/>
</>
)
}
注意点
next.config.jsの設定
next.config.js
にi18nに関する設定を追記していないか確認する。
他の構築方法を試す過程で追記し、削除し忘れていないか注意
翻訳ファイルはjson
翻訳ファイルはjson
にする。他のファイルの拡張子につられてjs
にしない。
参考記事
Discussion