This site recently added English (/en/) and Simplified Chinese (/zh/) alongside Japanese. When you visit the top page, it reads your browser’s language setting and automatically shows the English page to visitors set to English and the Chinese page to those set to Chinese.
This article explains the browser API at the heart of that behavior — navigator.language / navigator.languages — using the actual code running on this site as an example. Even a static site with no server-side processing can achieve this with a few dozen lines of JavaScript.
The browser holds a “wish list of languages”
Browsers such as Chrome and Safari have a setting for “which language you want to see content in” (in Chrome, “Settings → Languages”). It usually inherits the OS language setting, so many people may never have paid it any attention.
From JavaScript, this setting can be read through the following two properties.
navigator.language // the single top-priority language (e.g. "ja")
navigator.languages // a prioritized list of languages (e.g. ["ja", "en-US", "en"])As MDN explains, navigator.languages is a read-only array in which earlier entries have higher priority, and navigator.language matches its first value. For making decisions, navigator.languages — which lets you walk through the candidates in order — is the more convenient of the two.
The value format is a “language tag” — RFC 5646 (BCP 47)
The strings these properties return, such as "ja" or "en-US", are language tags defined in RFC 5646 (commonly known as BCP 47). They are hyphen-separated and have a structure like the following.
ja— language only (Japanese)en-US— language + region (American English)zh-CN— language + region (Mainland Chinese)zh-Hans— language + writing system (Simplified Chinese)
The key point is that even for the same language, the tag that arrives can vary. Chinese, for example, may arrive in various forms depending on the environment — zh, zh-CN, zh-TW, zh-Hans, and so on. For that reason, the code below takes the approach of looking only at “the leading primary-language part” rather than matching the tag exactly.
Note that the same information is also sent to the server as the HTTP request’s Accept-Language header (RFC 9110 §12.5.4). There is a way to decide on the server side and redirect, but on a site like ours that serves static files from a CDN, the standard approach is to decide on the browser side, because of caching-compatibility issues.
The actual code
The <head> of this site’s top page embeds the following script.
;(() => {
try {
// ① If the visitor returned to the top page from within the site, do nothing
if (document.referrer && new URL(document.referrer).origin === location.origin) return
const target = (l) => (l === 'en' ? '/en/' : l === 'zh' ? '/zh/' : null)
// ② A language explicitly chosen via the language switcher takes top priority
const stored = localStorage.getItem('preferred-lang')
if (stored) {
const t = target(stored)
if (t) location.replace(t)
return
}
// ③ Check the browser's prioritized language list from the top down
for (const l of navigator.languages || [navigator.language]) {
const s = (l || '').toLowerCase()
if (s.startsWith('ja')) return // If Japanese ranks high, show as-is
const t = target(s.slice(0, 2)) // "en-US" → "en", "zh-CN" → "zh"
if (t) { location.replace(t); return }
}
} catch {}
})()Alongside this, the language-switcher links in the footer carry a mechanism that “remembers the language you clicked.”
document.querySelectorAll('[data-lang-switch]').forEach((a) => {
a.addEventListener('click', () => {
try { localStorage.setItem('preferred-lang', a.dataset.langSwitch) } catch {}
})
})Points about the code
Prioritize the user’s intent over automatic detection (②) — There are always people who think, “I use an English-language browser, but I want to read in Japanese.” We save the language chosen via the switcher link to localStorage and, from the next visit onward, give it priority over automatic detection. If you are going to add an automatic redirect, pairing it with this mechanism is recommended.
Do not trigger on movement within the site (①) — Using document.referrer, we check “where the visitor came from,” and if it is a transition from within our own site, we skip the decision. Without this, the moment an English-set visitor who is reading a Japanese page clicks the logo, they would be sent off to the English top page.
Decide by the primary language only (③) — Because language tags come in variations, we extract only the primary language (en or zh) with slice(0, 2) for comparison. This way we can guide even a zh-TW visitor to the Chinese page.
Use location.replace() — Unlike assigning to location.href, the redirect source is not left in history, so you avoid the loop of being redirected again every time you press “Back.”
Wrap it in try...catch — This keeps errors from halting execution even in environments where localStorage is unavailable, such as private browsing. Even if this script does not run at all, the only consequence is that the Japanese page is shown and the manual language links remain usable — there is no real harm.
Consideration for search engines
Automatic language redirects, taken too far, risk preventing search engines from finding the pages for each language. So on this site, we do the following three things together.
- Limit the automatic redirect to the top page only (links to lower-level pages are shown as-is)
- Output
hreflangon every page to make the correspondence between language versions explicit (see Google’s documentation) - Use
x-defaultto indicate the default page for visitors who match none of the languages
<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/" />Summary
- The browser’s language setting can be read with
navigator.languages(the values are RFC 5646 language tags) - Even a static site can achieve automatic language routing with browser-side JavaScript alone
- Adding the three guards — “give the user’s explicit choice top priority,” “do not trigger on in-site transitions,” and “target the top page only” — makes for automatic detection that is not overbearing
At our digital-support business, we also take on consultations about multilingual support for websites like this. Please feel free to contact us.
