Nextjsのi18nで、あらゆる言語を徹底的にリダイレクトする
日本のウェブサイトでこんな実装をする機会は、ほぼないと思います。.jp
があるんですから、パスでi18nルーティングすること自体が希少です。
この記事では、何かしらの理由で全ページに言語コードの接頭辞を付ける場合の対処法を説明します。
ブラウザの設定 | Accept-Language |
ページ | 結果 | 備考 |
---|---|---|---|---|
日本語 | ja |
/ |
302 Found→/ja
|
|
日本語 | ja |
/en |
200 OK→/en
|
(転送されない) |
英語(米国) | en-US,en;q=0.9 |
/ |
302 Found→/en
|
|
英語(米国) | en-US,en;q=0.9 |
/ja |
200 OK→/ja
|
(転送されない) |
韓国語 | ko |
/ |
302 Found→/ja
|
|
韓国語 | ko |
/ja |
200 OK→/ja
|
(転送されない) |
韓国語 | ko |
/en |
200 OK→/en
|
(転送されない) |
例えば 「日本語と英語を定義し、日本語がデフォルト」 の場合こうなります。/ja
や/en
が付いていれば転送しません。
何をするか把握できた人は実装まで飛ばしてください。
いろいろなi18nルーティングの例
i18nルーティングには、以下の3パターンがあります。
- ドメインルーティング
- Prefix except edfault (デフォルト以外は接頭辞)
- Prefix (全部接頭辞)
- Prefix and default (デフォルトも接頭辞も)
1〜3はNuxtのi18nモジュールの説明から引用しました。
0. ドメインルーティング
例えば、「.jp」と「.us」を使えばいいのです。「.com」をアメリカに割り振る企業も多いですが、その場合/us
がついていたりします。
ぜんぜん違う話になってきますから、この記事ではドメインルーティングについて扱いません。
1. prefix_except_default (デフォルト以外は接頭辞)
英語中心のソフトウェアのウェブサイトに最適です。多国籍サイトを立ち上げるなら最も主流の方法でしょう。
言語コードも国コードも
例としてDocusaurusのウェブサイトを挙げます。
- https://docusaurus.io/ はアメリカ語版。
- https://docusaurus.io/ko/ は韓国語版。
- https://docusaurus.io/fr/ はフランス語版。
- https://docusaurus.io/zh-CN/ は简体中文版。
言語だけのルーティングも許容されています。
国コードだけ
例としてAppleのウェブサイトを挙げます。
- https://apple.com はアメリカ語版。
- https://apple.com/jp/ は日本語版。
- https://apple.com/us/ は404。
フィジカルな商売をする多国籍企業は、「国コードだけ」が最適解でしょう。
2. prefix (全部接頭辞)
トップ以外リダイレクトはしない
例としてGitHubのドキュメントを挙げます。
- https://docs.github.com/en はアメリカ語版。
- https://docs.github.com/ja は日本語版。
-
https://docs.github.com はアメリカからでも
/en
にリダイレクト(VPNで検証)。
トップページ以外ではリダイレクトはせず、「This article is also available in (ここにブラウザの言語)」という案内が出ます。
徹底的にリダイレクトする
例としてMicrosoftのウェブサイトを挙げます。
- https://www.microsoft.com/en-us/windows/ はアメリカ語版。
- https://www.microsoft.com/ja-jp/windows/ は日本語版
- https://www.microsoft.com/windows/ はアメリカからでもアメリカ語版にリダイレクト(VPNで検証)。
画像はVPNで米国からのアクセスを偽装し、OSもブラウザもアメリカ語にした場合の表示です。en-us
でインデックスされていますね。
3. prefix_and_default (デフォルトも接頭辞も)
Next.js (v12.0.7時点) のデフォルト挙動はこれです。
そして、奇妙なことにこの挙動の例が見つけられません。
/** @type {import('next').NextConfig} */
module.exports = {
i18n: {
locales: ['ja', 'en'],
defaultLocale: 'ja',
}
};
例えば、コーポレートサイトを英語に対応させる状況です。defaultLocale
をja
に設定しています。
pages/index.tsx
にこんにちは
またはHello
が出るとしましょう。
|URL|結果|
|---|---|---|
|/
|こんにちは|
|/ja
|こんにちは|
|/en
|Hello|
デフォルトロケールの関係で、日本語コンテンツが2箇所に存在していますね。
canonical
タグであるとはいえ、モバイル向けとかそういう分類ではないから、重複コンテンツじゃないか? と思ったのですが、実際に重複した状態で動いているサイトが見つけられませんでした。
Next.jsでも「全部接頭辞」パターンを使いたい
2020年10月、「すべてのロケールにprefix付けさせてくれ!」というディスカッションが始まりました。
そして問題提起からちょうど1年後 に、Next.js 12のMiddlewareを使った解決策が投稿され、ちょっと改良されたものが「アンサー」としてマークされました。
でも全ページで言語検知したい
Next.jsのi18nでは、トップページ以外でlocaleDetection
が動きません。
For example what if a crawler did not set Accept-Language and goes to /nl/blog/hello-world, we'd want it to be indexed and not redirected to /en/blog/hello-world (if en is the defaultLanguage).
これは、言語指定のないクローラーが「デフォルト」に飛ばされ、翻訳ページをガン無視してしまうのを回避するためです。
その結果、上記の実装だと、トップページ以外ではブラウザ言語より/ja
が優先されてしまいます。
私はマイクロソフトのように完全なリダイレクトをしたいため、accept-language-parser
というパッケージで解決します。
実装
パッケージ
yarn add -D @types/accept-language-parser
yarn add accept-language-parser
言語だけを取り出したいので、これが今回の実装では一番都合がいいです。
コンフィグ
module.exports = {
i18n: {
locales: ['default', 'ja', 'en'],
defaultLocale: 'default',
},
}
まず、default
というダミーロケールを作ります。
こうしないと、リダイレクトしたか否かの判定ができないため、無限に転送されてしまいます。
import { NextRequest, NextResponse } from 'next/server';
import acceptLanguageParser from 'accept-language-parser';
/**
* 選択肢の中から言語を選ぶ
* @param localeHeader Accept-Languageヘッダの値
* @returns 言語だけのコード
*/
function selectLanguage(localeHeader: string | null) {
// 選択肢
const languages = ['ja', 'en'];
const detectedLang = acceptLanguageParser.pick(
languages,
localeHeader ?? 'ja-JP',
{ loose: true }
);
// いずれも該当しない場合はjaを選ぶ
return detectedLang ?? languages[0];
}
/**
* どんな言語でも接頭辞を付ける
* @param request
* @see https://nextjs.org/docs/advanced-features/i18n-routing#prefixing-the-default-locale 参考
* @see next.config.js で「default」をデフォルトロケールにしている
* @returns
*/
export function middleware(request: NextRequest) {
const { pathname, locale, search } = request.nextUrl;
// ファイルは除外
const PUBLIC_FILE = /\.(.*)$/;
const shouldHandleLocale =
!PUBLIC_FILE.test(pathname) &&
!pathname.includes('/api/') &&
locale === 'default';
const language = selectLanguage(request.headers.get('accept-language'));
return shouldHandleLocale
? NextResponse.redirect(`/${language}${pathname}${search}`)
: undefined;
}
そしてミドルウェアの登場です。_middleware.ts
を用意します。 (pageExtensions
を指定している場合_middleware.page.ts
などにする必要があります。)
{ loose: true }
を指定することで、言語コードだけが返ってくるようにしています。
検証
Accept-Language
ヘッダを使って振り分けます。Chrome 96.0.4664.110で検証しました。
ブラウザの設定 | Accept-Language |
ページ | 結果 | 備考 |
---|---|---|---|---|
日本語 | ja |
/ |
302 Found→/ja
|
|
日本語 | ja |
/en |
200 OK→/en
|
(転送されない) |
英語(米国) | en-US,en;q=0.9 |
/ |
302 Found→/en
|
|
英語(米国) | en-US,en;q=0.9 |
/ja |
200 OK→/ja
|
(転送されない) |
韓国語 | ko |
/ |
302 Found→/ja
|
|
韓国語 | ko |
/ja |
200 OK→/ja
|
(転送されない) |
韓国語 | ko |
/en |
200 OK→/en
|
(転送されない) |
/ja
や/en
が付いていればlocale
がそれに固定されるので、まず転送が発火しません。
ブラウザの設定 | Accept-Language |
ページ | 結果 |
---|---|---|---|
韓国、英語(英国) | ko,en-GB;q=0.9,en;q=0.8 |
/ |
302 Found→/en
|
韓国語、日本語、英語(米国) | ko,ja;q=0.9,en-US;q=0.8,en;q=0.7 |
/ |
302 Found→/ja
|
複数の言語を指定している場合は、上から順に選ばれます。
localeDetectionを切る必要性が分からない
上記のディスカッションでもドキュメントの例でもlocaleDetection: false
ですが、私は切っていません。
どの言語にしても、ちゃんとdefault
ロケールから飛ばされています。localeDetection、切らなくていいのでは?
同じこと疑問に思ってる方がいたのですが、「推奨されてるから切ってるけど調査中」とのこと。
注意点
next-i18nextを使う場合
default
ロケールを捏造した関係で、public/locales/default
がないと怒られます。
既存のサイトを改修する場合
既存のサイトをこの方法に切り替えたことによるSEOへの影響は把握していませんし責任を負えません。
NextResponse.redirect
にステータスを渡さない限り 302リダイレクト(一時的な移動) になります。言語切替としては正しいですが、「永久移行」ではありません。
Discussion