本站日前在日语之外,新增了对英语(/en/)和简体中文(/zh/)的支持。访问首页时,会读取浏览器的语言设置,向英语设置的访客自动显示英文页面,向中文设置的访客自动显示中文页面。

本文以本站实际运行的代码为例,讲解位于其核心的 navigator.language / navigator.languages 这一浏览器 API。即使是没有服务器端处理的静态站点,也能用几十行 JavaScript 实现。

浏览器持有一份「语言愿望清单」

Chrome、Safari 等浏览器都有一项「希望以何种语言查看内容」的设置(在 Chrome 中为「设置 → 语言」)。它通常沿用操作系统的语言设置,因此许多人可能从未留意过。

在 JavaScript 中,可以通过以下两个属性读取该设置。

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 确认「从何处而来」,若是来自本站内部的跳转,则跳过判断。若无此项,英语设置的访客在阅读日文页面时一点击 Logo,就会立刻被送往英文首页。

仅凭主语言判断(③) —— 由于语言标签存在变体,用 slice(0, 2) 仅取出主语言(enzh)进行比较。如此便能将 zh-TW 的访客也引导至中文页面。

使用 location.replace() —— 与向 location.href 赋值不同,重定向的来源不会留在历史记录中,因此不会发生每按一次「后退」就再次被重定向的循环。

try...catch 包裹 —— 这样即使在私密浏览等无法使用 localStorage 的环境中,也不会因报错而中止。即便该脚本完全不运行,后果也仅仅是显示日文页面、手动的语言链接仍可使用而已,并无实质危害。

对搜索引擎的顾及

按语言自动重定向若做得过头,有可能导致搜索引擎无法找到各语言的页面。为此,本站同时做了以下三点。

  • 将自动重定向仅限于首页(指向下层页面的链接照原样显示)
  • 在所有页面输出 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 也能实现语言的自动分流
  • 添加「最优先用户的明确选择」「站内跳转时不触发」「仅以首页为对象」这三道防护,便能形成不带强迫感的自动判断

在本公司的数字支持业务中,我们也承接此类网站多语言支持的咨询。欢迎随时与我们联系

参考资料