# General Translation Knowledge Base: ユーザーのブラウザ言語を検出するには？
URL: https://generaltranslation.com/ja/kb/detect-user-language.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: ブラウザが持つ優先順位付きの言語リスト（JavaScript では navigator.languages、サーバー側では q 値で並べ替えた Accept-Language ヘッダー）を読み取り、フォールバックを用意したうえで対応ロケールとマッチングします。なお、Safari や Chrome のシークレットモードでは 1 つの言語しか通知されません。また、IP ジオロケーションでわかるのは所在地であって、言語ではありません。
Category: 国際化
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

答えはすでにブラウザの中にあります。ユーザーがブラウザや OS で設定した、優先順位付きの言語リストです。やるべきことは、このリストを正しく読み取り、翻訳を用意しているロケールとマッチングすること、そしてより確かなシグナルを優先すべき場面を見極めることです。

## ユーザーの言語設定はどこから来るのか？

表示する言語を判断する手がかりとなるシグナルは5つあります。ただし、言語そのものに関するシグナルはその一部だけです。

| シグナル                   | 取得元               | わかること               | 信頼性                           | 用途                           |
| ---------------------- | ----------------- | ------------------- | ----------------------------- | ---------------------------- |
| `navigator.languages`  | ブラウザの JavaScript  | 優先順位付きの言語設定         | 良好。ただし、1つの言語しか公開しないブラウザもある    | 初回訪問時のクライアントでレンダリングされるページ    |
| `Accept-Language` ヘッダー | すべての HTTP リクエスト   | 同じリストを q 値で重み付けしたもの | 良好。ただし、旅行中などはユーザーが変更できないことが多い | 初回訪問時のサーバーでレンダリングされたページ      |
| URL 内のロケール             | `/fr/` などのリクエストパス | 訪問者が要求したバージョン       | 高い                            | 公開され、共有可能なページ                |
| 保存された設定                | クッキーまたはアカウント設定    | ユーザーが以前に選択した内容      | 高い                            | 再訪問時                         |
| IP ジオロケーション            | 位置情報の参照サービス       | おおよその位置             | 言語のシグナルではない                   | 通貨などのリージョン別デフォルト (言語には使わない)  |

### ブラウザの場合: `navigator.language` と `navigator.languages`

[`navigator.languages`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages) は、ユーザーの優先言語を BCP 47 タグで優先度の高い順に返します。[`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language) は、その配列の先頭の要素です:

```js
navigator.language;  // "en-US"
navigator.languages; // ["en-US", "zh-CN", "ja-JP"]
```

ユーザーが設定を変更すると、ブラウザは `window` 上で `languagechange` イベントを発火します。なお、フィンガープリンティング対策として、Safari は常に 1 つの言語しか返さず、シークレットモードの Chrome も同様です。そのため、このリストにはユーザーが設定したすべての言語が含まれていない可能性がある点に注意してください。

### サーバー側：`Accept-Language` ヘッダー

ブラウザは、リクエストのたびに同じ言語設定を送信します。[RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language) では、このヘッダーを言語範囲のカンマ区切りリストとして定義しており、各言語範囲には 0 から 1 までの品質値 (`q`) を任意で付けられます。`q` を省略した場合は 1 とみなされ、`q=0` は「受け入れ不可」を意味します。RFC 自体に記載されている例は次のとおりです。

```http
Accept-Language: da, en-gb;q=0.8, en;q=0.7
```

これは「デンマーク語を優先するが、イギリス英語やその他の英語も受け入れる」という意味です。`*` ワイルドカードは、列挙されていない任意の言語を[表します](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language)。ブラウザは言語のみのフォールバックも追加します。[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages)によると、`navigator.languages` が `["en-US", "zh-CN"]` のユーザーの場合、Chrome と Safari は `en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7` を送信します。RFC では列挙順に「依存できない」とされているため、エントリは `q` の値で並べ替えてください。

このヘッダーはあくまでヒントにすぎません。MDN は、サーバーは「ユーザーが明示的に選択した言語を決して上書きすべきではない」としたうえで、旅行中など、ヘッダーをユーザー自身が制御できない場合が多いことも指摘しています。ヘッダーに基づいて選択したページをキャッシュする場合は、[`Vary: Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary) を追加し、ある訪問者向けの言語のページが別の訪問者に返されないようにしてください。

### IPジオロケーションが判断材料として不適切な理由

所在地と言語は同じではありません。W3Cも[指摘している](https://www.w3.org/International/questions/qa-link-lang)ように、「多くの国には複数の公用語がある」うえ、旅行者や国外居住者、VPNの利用者は、IPアドレスからは予測できない言語を使っています。Googleもこの点をはっきり述べており、[多地域サイトに関するガイダンス](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites)で「IP分析を使ってコンテンツを調整しないでください」としています。理由の一つは、「Googlebotクローラーは通常米国からアクセスする」ことです。IPに基づいて言語を選ぶページは、クローラーに1つのバージョンしか見せず、それ以外を隠してしまいます。所在地は、デフォルトの通貨や配送リージョンを適切に選ぶ目的であれば引き続き活用できますが、その判断は言語の選択とは切り離してください。

## ユーザーの希望をロケールと照合する

`pt-BR` を提供しているサイトで `pt-PT` を要求するユーザーには、フォールバックのルールが必要です。アルゴリズムは 2 種類あり、[`Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) でも同じ名前が使われています。

* **参照** は [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4) に準拠します。希望するタグを順に試し、一致するまで末尾からサブタグを削っていき (`zh-Hant-CN`、`zh-Hant`、`zh` の順) 、どれも一致しなければデフォルトを返します。
* **Best fit** では、参照と比べて「少なくとも同等、場合によってはそれ以上に適した」ロケールを実装が返すことができます。たとえば、同じ言語の別のリージョンバリアントなどです。

この違いはリージョンバリアントで顕著に現れます。[`@formatjs/intl-localematcher`](https://www.npmjs.com/package/@formatjs/intl-localematcher) 0.9.0 を使い、`['en', 'fr', 'de', 'pt-BR']`、デフォルト `en` で照合した結果は次のとおりです。

| 要求            | 参照 | Best fit |
| ------------- | ------ | -------- |
| `pt-PT`       | `en`   | `pt-BR`  |
| `en-GB`       | `en`   | `en`     |
| `fr-CA`, `de` | `fr`   | `fr`     |
| `zh-TW`       | `en`   | `en`     |

参照は挙動が予測しやすく、説明も簡単です。一方、Best fit はより多くのユーザーに読める言語を届けられます。ただし、ポルトガル在住のポルトガル語話者が英語よりブラジルポルトガル語を好むかどうかはプロダクトとして判断すべき事項なので、実際のロケール一覧で両方を試してみてください。

## 例：サーバーでネゴシエーションする

この Node.js サーバーは、URL、保存済みのクッキー、ヘッダーの順に確認します。[`negotiator`](https://www.npmjs.com/package/negotiator) 1.1.0 で `Accept-Language` を解析して q 値順に並べ替え、`@formatjs/intl-localematcher` でその結果を best fit 方式で照合します。

```bash
npm install negotiator @formatjs/intl-localematcher
```

```js title="server.mjs"
import { createServer } from 'node:http';
import Negotiator from 'negotiator';
import { match } from '@formatjs/intl-localematcher';

const SUPPORTED = ['en', 'fr', 'de', 'pt-BR'];
const DEFAULT_LOCALE = 'en';

// "*" や不正な形式のタグは、match() が RangeError をスローするため除外する。
function isValidTag(tag) {
  try {
    Intl.getCanonicalLocales(tag);
    return true;
  } catch {
    return false;
  }
}

function pickLocale(req) {
  // 1. URL パス内のロケール（例: /fr/pricing）
  const { pathname } = new URL(req.url, 'http://localhost');
  const fromPath = pathname.split('/')[1];
  if (SUPPORTED.includes(fromPath)) return { locale: fromPath, source: 'url' };

  // 2. 以前の訪問時に保存された選択
  const fromCookie = /(?:^|;\s*)locale=([^;]+)/.exec(req.headers.cookie ?? '')?.[1];
  if (SUPPORTED.includes(fromCookie)) return { locale: fromCookie, source: 'cookie' };

  // 3. Accept-Language を q 値で並べ替え、サポート対象のロケールと照合
  const requested = new Negotiator(req).languages().filter(isValidTag);
  if (requested.length === 0) return { locale: DEFAULT_LOCALE, source: 'default' };
  return { locale: match(requested, SUPPORTED, DEFAULT_LOCALE), source: 'header' };
}

createServer((req, res) => {
  const { locale, source } = pickLocale(req);
  res.setHeader('Content-Language', locale);
  res.setHeader('Vary', 'Accept-Language, Cookie');
  res.end(`${locale} (from ${source})\n`);
}).listen(3000);
```

`node server.mjs` で起動し、`curl -H "Accept-Language: ..." localhost:3000/` でリクエストを送信します。Node.js 24 での出力は次のとおりです。

```text
da, en-gb;q=0.8, en;q=0.7                      -> en (from header)
pt-PT,pt;q=0.9,en;q=0.8                        -> pt-BR (from header)
fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5   -> fr (from header)
ja, de;q=0.5                                   -> de (from header)
en;q=0.5, de                                   -> de (from header)
*                                              -> en (from default)
(no header)                                    -> en (from default)
fr, with cookie locale=de                      -> de (from cookie)
/fr/pricing, with cookie locale=de             -> fr (from url)
```

`isValidTag` によるフィルタリングは欠かせません。これがないと、ヘッダーに `*` が含まれている場合に `match()` が `RangeError: Incorrect locale information provided` をスローし、リクエストがクラッシュしてしまいます。

## 例: ブラウザでの検出

クライアントでレンダリングされるページでは、同じ考え方を `navigator.languages` にも適用できます。次の例では、依存関係を使わずに RFC 4647 のシンプルな参照を実装しています:

```js title="detect-locale.js"
const SUPPORTED = ['en', 'fr', 'de', 'pt-BR'];
const DEFAULT_LOCALE = 'en';

// Lookup: 優先タグを順に試し、一致しなければ末尾のサブタグを削って再試行します。
function lookup(requested, supported, fallback) {
  const lower = supported.map((tag) => tag.toLowerCase());
  for (const tag of requested) {
    const parts = tag.split('-');
    while (parts.length) {
      const i = lower.indexOf(parts.join('-').toLowerCase());
      if (i !== -1) return supported[i];
      parts.pop();
    }
  }
  return fallback;
}

function preferredLocale(nav) {
  const requested = nav.languages?.length ? nav.languages : [nav.language];
  return lookup(requested, SUPPORTED, DEFAULT_LOCALE);
}

// ブラウザでの使用例: preferredLocale(navigator)
```

サンプルの優先言語リストに対する出力 (Node.js 24 で、`navigator` オブジェクトの代替を用意して実行) ：

```text
["fr-CA","en-US"] -> fr
["pt-BR","pt"]    -> pt-BR
["pt-PT","en"]    -> en
["ja-JP"]         -> en
["de-AT","de"]    -> de
```

`preferredLocale` を呼び出す前に、保存済みの選択がないか確認してください。また、`preferredLocale` の結果はあくまで初期値として扱い、その言語でレンダリングしたうえで言語切り替えを表示します。この結果をもとにリダイレクトしないでください。

## どのシグナルを優先すべきか？

言語検出は、初回訪問時の推測にすぎません。シグナル同士が食い違う場合は、次の優先順位に従います。

1. **URL。** 共有された `/de/` リンクではドイツ語を表示し、言語切り替え機能で言語を選択した場合はその言語の URL に遷移します。
2. **保存された選択。** クッキーやアカウント設定です。言語中立なルートからの遷移先や提案する言語の決定に使いますが、ローカライズ済みの URL を書き換えることはありません。
3. **`Accept-Language` ヘッダー** (クライアント側では `navigator.languages`) 。保存された選択がない場合にのみ使用し、用途はルートでの判定か提案に限ります。
4. **デフォルトロケール。** どれにも一致しない場合のフォールバックです。

W3C は、ヘッダーを[出発点](https://www.w3.org/International/questions/qa-accept-lang-locales)として使い、ユーザーが言語を変更できるようにしたうえで、その結果を「次回以降の訪問のためにデータベースまたはクッキーに」保存することを推奨しています。この優先順位に基づくリダイレクト、キャッシュ、SEO については[ロケールルーティングと SEO](/kb/locale-routing-seo) を、切り替え機能自体の実装については[言語切り替え機能の構築](/kb/language-switcher)を参照してください。

## gt-next、gt-react、gt-node がロケールを解決する仕組み

General Translation の `gt-next` では、[`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) が各リクエストのロケールを次の順序で解決します。訪問者が直前に選択したロケール (リセット用クッキーで追跡) 、URL のロケール (`/es/about` など) 、保存済みのロケールクッキー、直前のクライアントルートから引き継いだリファラーロケールクッキー、`Accept-Language` ヘッダー、そして最後に `defaultLocale` です。解決結果はデフォルトで `generaltranslation.locale` クッキーに保存されます。クッキー名は [`headersAndCookies`](/docs/react/nextjs/config#headers-and-cookies) で変更できます。ヘッダーを完全に無視するには、[`ignoreBrowserLocales`](/docs/react/nextjs/config#ignore-browser-locales) を設定してください。

要求されたロケールがサポートされていない場合、ライブラリはまずブラウザの優先言語の中からサポート対象で最も近いものを選び、見つからなければ同じ言語のロケール (`en-US` に対する `en-GB` など) 、それもなければデフォルトを選択します。結果は [`useLocale`](/docs/react/reference/hooks/use-locale) で取得できます。非同期コードでは `gt-next/server` の [`getLocale`](/docs/react/nextjs/reference/functions/get-locale) を使用します。

React のシングルページアプリでは、`gt-react` の [`initializeGTSPA`](/docs/react/reference/config#initialize-spa) が初回レンダリングの前にクッキーとブラウザの設定からロケールを解決します。Node.js サーバーでは、`gt-node` の [`getRequestLocale`](/docs/node/reference/functions/get-request-locale) が、[`initializeGT`](/docs/node/reference/functions/initialize-gt) に渡されたロケールと `Accept-Language` を照合します。たとえば `en`、`fr`、`de`、`pt-BR` を設定した場合、`fr-CH, fr;q=0.9, en;q=0.8` に対しては `fr` が、`pt-PT,pt;q=0.9` に対しては `pt-BR` が返されました。[gt-node のロケールガイド](/docs/node/guides/detecting-locale)にあるように、その前段でクッキーやパスによる判定を行ってください。

## 重要なポイント

* `navigator.languages` と `Accept-Language` には、同じ優先順位付きの言語設定が含まれます。ただし、Safari と Chrome のシークレットモードでは 1 つの言語しか公開されません。
* マッチングの前に q 値 (0〜1、デフォルトは 1) を解析して並べ替え、`*` や不正な形式のタグを除外してください。`@formatjs/intl-localematcher` は `*` を渡すと例外をスローします。
* `pt-BR` しかない場合、参照では `pt-PT` がデフォルトにマッピングされ、best fit では `pt-BR` にマッピングされます。どちらを使うかは意識的に選びましょう。
* 言語は URL、保存された選択、`Accept-Language`、デフォルトの順に解決します。IP ジオロケーションを言語のシグナルとして使ってはいけません。
* `gt-next` は、直前に選択されたロケールの次に、URL、`generaltranslation.locale` クッキー、`Accept-Language`、`defaultLocale` の順にチェックします。

## Related questions

- [言語リダイレクトと SEO：URL、クッキー、Accept-Language のどれを使うべきか](https://generaltranslation.com/ja/kb/locale-routing-seo.mdx): Googlebot は Accept-Language ヘッダーを送信せず、クロールの大半を米国の IP アドレスから行います。そのため、インデックスさせたいすべてのページの言語は URL で決めましょう。保存済みのクッキーの選択、次いで Accept-Language を使うのは、/ のような中立的なルートのリダイレクト先を決める場合だけにとどめ、その際は一時的な 302 または 307 を使います。
- [言語切り替え機能のベストプラクティス：UX、SEO、アクセシビリティ](https://generaltranslation.com/ja/kb/language-switcher.mdx): 各言語は Deutsch や 日本語 のようにその言語自身の名称で表記し、国を表す国旗は使わないでください。各選択肢からは、クロール可能なリンクで同じページの該当言語版へ遷移させ、選択内容はクッキーに保存します。ローカライズされた URL は決して自動リダイレクトしないでください。
- [hreflang とは？設定方法を解説](https://generaltranslation.com/ja/kb/what-is-hreflang.mdx): hreflang は、どの URL が同じページの言語版または地域版であるかを Google に伝え、検索結果に適切なページを表示させるための仕組みです。HTML の link タグ、HTTP の Link ヘッダー、またはサイトマップで追加し、すべてのリンクを相互に参照させたうえで、en-UK ではなく en-GB のような有効なコードを使用してください。
- [ウェブサイトを多言語化するにはどうすればよいですか？](https://generaltranslation.com/ja/kb/make-website-multilingual.mdx): 翻訳をどこで管理するか（プロキシ、CMSプラグイン、サイトビルダー、または自前のコード）を選び、Googleの推奨どおり言語ごとに専用のURLを用意します。ページ全体に加えてタイトルとメタディスクリプションも翻訳し、hreflangで各言語版を関連付けます。また、訪問者を自動でリダイレクトするのではなく、言語切り替え機能を設置しましょう。

## Previous and next questions

- Previous question: [ウェブサイトを多言語化するにはどうすればよいですか？](https://generaltranslation.com/ja/kb/make-website-multilingual.mdx)
- Next question: [国際化とローカライゼーションの違い（i18n vs. l10n）](https://generaltranslation.com/ja/kb/internationalization-vs-localization.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/ja/kb/sitemap.md) for every article.
