当サイトは先日、日本語に加えて英語(/en/)と簡体字中国語(/zh/)に対応しました。トップページにアクセスすると、ブラウザの言語設定を読み取って、英語設定の方には英語ページを、中国語設定の方には中国語ページを自動的に表示します。

この記事では、その中心にある navigator.language / navigator.languages というブラウザのAPIについて、実際に当サイトで動いているコードを例に解説します。サーバー側の処理を持たない静的サイトでも、数十行のJavaScriptで実現できます。

ブラウザは「言語の希望リスト」を持っている

Chrome や Safari などのブラウザには、「どの言語でコンテンツを見たいか」という設定があります(Chrome では「設定 → 言語」)。通常は OS の言語設定を引き継いでいるため、意識したことがない方も多いかもしれません。

JavaScript からは、この設定を次の2つのプロパティで読み取れます。

navigator.language   // 最優先の言語ひとつ(例: "ja")
navigator.languages  // 優先順位つきの言語リスト(例: ["ja", "en-US", "en"])

MDN の解説にあるとおり、navigator.languages は先頭ほど優先度が高い読み取り専用の配列で、navigator.language はその先頭の値と一致します。判定には、候補を順に見ていける navigator.languages のほうが便利です。

値の形式は「言語タグ」 — RFC 5646(BCP 47)

これらのプロパティが返す "ja""en-US" という文字列は、RFC 5646(通称 BCP 47)で定められた言語タグです。ハイフン区切りで、次のような構造をしています。

  • ja — 言語のみ(日本語)
  • en-US — 言語+地域(アメリカの英語)
  • zh-CN — 言語+地域(中国本土の中国語)
  • zh-Hans — 言語+文字体系(簡体字の中国語)

ポイントは、同じ言語でも届くタグにはバリエーションがあることです。たとえば中国語は zhzh-CNzh-TWzh-Hans など、環境によってさまざまな形で届きます。そのため後述のコードでは、タグの完全一致ではなく「先頭の主言語部分だけを見る」方針にしています。

なお、同じ情報は HTTP リクエストの Accept-Language ヘッダー(RFC 9110 §12.5.4)としてサーバーにも送られています。サーバー側で判定してリダイレクトする方法もありますが、当サイトのように CDN から静的ファイルを配信するサイトでは、キャッシュとの相性の問題があるため、ブラウザ側で判定するのが定石です。

実際のコード

当サイトのトップページの <head> には、次のスクリプトを埋め込んでいます。

;(() => {
  try {
    // ① サイト内からトップへ戻ってきた場合は何もしない
    if (document.referrer && new URL(document.referrer).origin === location.origin) return

    const target = (l) => (l === 'en' ? '/en/' : l === 'zh' ? '/zh/' : null)

    // ② 言語切替で明示的に選ばれた言語があれば最優先
    const stored = localStorage.getItem('preferred-lang')
    if (stored) {
      const t = target(stored)
      if (t) location.replace(t)
      return
    }

    // ③ ブラウザの優先言語リストを上から順に判定
    for (const l of navigator.languages || [navigator.language]) {
      const s = (l || '').toLowerCase()
      if (s.startsWith('ja')) return    // 日本語が上位なら、このまま表示
      const t = target(s.slice(0, 2))   // "en-US" → "en"、"zh-CN" → "zh"
      if (t) { location.replace(t); return }
    }
  } catch {}
})()

あわせて、フッターの言語切替リンクには「クリックした言語を記憶する」仕掛けを入れています。

document.querySelectorAll('[data-lang-switch]').forEach((a) => {
  a.addEventListener('click', () => {
    try { localStorage.setItem('preferred-lang', a.dataset.langSwitch) } catch {}
  })
})

コードのポイント

利用者の意思を自動判定より優先する(②) — 「英語のブラウザを使っているけれど日本語で読みたい」という方は必ずいらっしゃいます。言語切替リンクで選んだ言語を localStorage に保存しておき、次回以降は自動判定より優先します。自動リダイレクトを入れるなら、この仕組みとセットにするのがおすすめです。

サイト内の移動では発動させない(①)document.referrer で「どこから来たか」を確認し、自サイト内からの遷移なら判定をスキップします。これがないと、英語設定の方が日本語ページを読んでいてロゴをクリックした瞬間、英語トップへ飛ばされてしまいます。

主言語だけで判定する(③) — 言語タグにはバリエーションがあるため、slice(0, 2) で主言語(enzh)だけを取り出して比較します。これで zh-TW の方にも中国語ページをご案内できます。

location.replace() を使うlocation.href への代入と違ってリダイレクト元が履歴に残らないため、「戻る」を押すたびにまたリダイレクトされる、というループが起きません。

try...catch で包む — プライベートブラウジングなど localStorage が使えない環境でもエラーで止まらないようにします。仮にこのスクリプトが一切動かなくても、日本語ページが表示されて手動の言語リンクが使えるだけなので、実害はありません。

検索エンジンへの配慮

言語による自動リダイレクトは、やりすぎると検索エンジンが各言語のページを見つけられなくなる恐れがあります。そこで当サイトでは、次の3点をあわせて行っています。

  • 自動リダイレクトはトップページだけに限定する(下層ページへのリンクはそのまま表示する)
  • 全ページに hreflang を出力し、各言語版の対応関係を明示する(Google のドキュメント参照)
  • どの言語にも当てはまらない訪問者向けの既定ページを x-default で伝える
<link rel="alternate" hreflang="ja" href="https://kobayaxi.com/" />
<link rel="alternate" hreflang="en" href="https://kobayaxi.com/en/" />
<link rel="alternate" hreflang="zh-CN" href="https://kobayaxi.com/zh/" />
<link rel="alternate" hreflang="x-default" href="https://kobayaxi.com/" />

まとめ

  • ブラウザの言語設定は navigator.languages で読み取れます(値は RFC 5646 の言語タグ)
  • 静的サイトでも、ブラウザ側の JavaScript だけで言語の自動振り分けを実現できます
  • 「利用者の明示的な選択を最優先する」「サイト内遷移では発動しない」「対象はトップページのみ」の3つのガードを添えると、押しつけがましくない自動判定になります

当社のデジタル支援事業では、こうしたWebサイトの多言語対応のご相談も承っています。お気軽にお問い合わせください。

参考