Chapter 25: Localization

We put servers near users to reduce latency. But latency is not just measured in milliseconds — it is also measured in comprehension. If a reader must mentally translate every sentence from a foreign language, the “round-trip time” of understanding explodes. Localization is the practice of adapting a system to a particular language, region, and cultural conventions. Just as we replicate data to bring it closer to users (see Chapter 24: Geo Replication), we replicate content in the user's native language to bring understanding closer to them.

Internationalization (i18n) is the engineering work that makes localization possible — structuring code so that locale-specific content can be swapped without changing logic. Localization (l10n) is the act of producing content for a specific locale. This chapter covers both.

Locale Detection

The first question a localized system must answer is: what language does this user want? We use a detection chain that checks multiple signals in priority order:

fn detect_lang(path: &str, headers: &str) -> (Lang, &str) {
    // 1. URL prefix: /ja/chapter/systems → Lang::Ja
    if path.starts_with("/ja/") {
        return (Lang::Ja, &path[3..]);
    }
    if path == "/ja" {
        return (Lang::Ja, "/");
    }

    // 2. For the landing page only: cookie, then Accept-Language
    if path == "/" {
        if let Some(lang) = parse_cookie(headers, "lang") {
            if lang == "ja" {
                return (Lang::Ja, path);
            }
        }
        let lang = parse_accept_language(headers);
        return (lang, path);
    }

    // 3. All other paths: no /ja/ prefix means English
    (Lang::En, path)
}

The chain follows a principle: the URL is the source of truth. A /ja/ prefix is an explicit choice — the user clicked a link in a specific language. For non-prefixed paths like /chapter/systems, the language is always English, regardless of cookies or browser settings. This ensures the language switcher works reliably: clicking “English” on a Japanese page always produces an English page.

The one exception is the landing page (/), where there is no content path to disambiguate. Here we check the lang cookie (set by a previous explicit language choice) and fall back to the browser’s Accept-Language header.

URL-Based Routing

We chose URL prefixes (/ja/chapter/systems) over query parameters (/chapter/systems?lang=ja) for language routing. There are several reasons for this:

Cacheability. CDNs and reverse proxies cache by URL path. Separate paths mean the English and Japanese versions each get their own cache entry without special configuration. Query parameters are often stripped or ignored by caches.

SEO. Search engines treat different URL paths as distinct pages. Combined with hreflang alternate tags, this allows Google to serve the Japanese version to Japanese-language searchers and the English version to English-language searchers:

<link rel="alternate" hreflang="en" href="https://p.jjm.net/chapter/systems">
<link rel="alternate" hreflang="ja" href="https://p.jjm.net/ja/chapter/systems">
The hreflang tag tells search engines that two pages are translations of each other. Without these tags, a search engine might penalize the Japanese page as duplicate content.

Shareability. A user can share /ja/chapter/discovery with a colleague and the recipient sees the Japanese version immediately, without needing to change any settings.

Content Module Structure

Each language has its own content module that mirrors the English original. The English content lives in content.rs and the Japanese translation in content_ja.rs. Both modules expose identical function signatures:

// content.rs (English)
pub fn chapter_systems() -> &'static str { r#"..."# }
pub fn chapter_discovery() -> &'static str { r#"..."# }
// ... 47 content functions

// content_ja.rs (Japanese)
pub fn chapter_systems() -> &'static str { r#"..."# }
pub fn chapter_discovery() -> &'static str { r#"..."# }
// ... 47 content functions (translated prose, English code)

A dispatch function routes to the correct module based on the detected language:

#[derive(Clone, Copy, PartialEq)]
enum Lang { En, Ja }

fn content_for(lang: Lang, slug: &str) -> &'static str {
    match lang {
        Lang::En => match slug {
            "foreword" => content::foreword(),
            "systems" => content::chapter_systems(),
            // ...
            _ => "<h1>Not Found</h1>",
        },
        Lang::Ja => match slug {
            "foreword" => content_ja::foreword(),
            "systems" => content_ja::chapter_systems(),
            // ...
            _ => "<h1>Not Found</h1>",
        },
    }
}
The &'static str return type means all content is compiled into the binary. There is no runtime file I/O, no template rendering, no database query. The content is as fast to serve as a static file — because it is a static string.

The Language Switcher

The sidebar contains a language switcher link that lets readers toggle between English and Japanese at any time. The link always points to the same page in the other language:

// In the sidebar, after the nav entries:
let switch_href = format!("{}{}", lang.other().prefix(), active_href);
let switch_label = match lang {
    Lang::En => "日本語",
    Lang::Ja => "English",
};
// Renders: <a href="/ja/chapter/localization">日本語</a>

When a reader clicks the language switcher, two things happen:

First, the URL changes to include (or remove) the /ja/ prefix, so the browser navigates to the translated page. Second, a lang cookie is set to record the preference:

// Visiting /ja/... sets the Japanese preference
Set-Cookie: lang=ja; Path=/; Max-Age=31536000

// Visiting any non-prefixed book page clears it
Set-Cookie: lang=en; Path=/; Max-Age=31536000

The cookie only influences the landing page (/). For all other pages, the URL prefix (or its absence) is the sole authority on language. This means a Japanese reader who visits p.jjm.net directly will see the Japanese landing page on their next visit, without needing to navigate to /ja/ first. But clicking “English” on any page always takes them to English, regardless of the cookie.

Try it now: Click 日本語 in the sidebar to see this chapter in Japanese. Notice how the URL changes to /ja/chapter/localization and all navigation labels switch to Japanese. Click English to switch back.

Accept-Language Parsing

On the landing page, when no cookie is present, we fall back to the browser's Accept-Language header. This header contains a comma-separated list of language tags with optional quality values:

Accept-Language: ja;q=0.9, en-US;q=0.8, en;q=0.7

fn parse_accept_language(headers: &str) -> Lang {
    let mut ja_q: f32 = 0.0;
    let mut en_q: f32 = 0.0;
    // Parse each language tag and its quality value
    // "ja;q=0.9" → ja_q = 0.9
    // "en"       → en_q = 1.0 (default quality)
    if ja_q > en_q { Lang::Ja } else { Lang::En }
}
Quality values range from 0 to 1, with 1 being the default when q= is omitted. A header of ja, en;q=0.5 means “I prefer Japanese (quality 1.0) but will accept English (quality 0.5).”

Translation as Infrastructure

A common mistake is to treat translation as an afterthought — building the entire system in one language and then scrambling to bolt on translations later. The better approach is to build the localization pipeline first:

1. Design the Lang enum and detection chain.

2. Build the content dispatch function and parallel module structure.

3. Add the language switcher and hreflang tags.

4. Then produce translations.

This way, the infrastructure is tested and working before any translation begins. When translations arrive, they slot into the existing module and immediately work — URL routing, cookie persistence, SEO tags, and the language switcher all function without additional code changes.

This principle mirrors how we approach all infrastructure in this book: build the system first, then let it serve its purpose. The localization infrastructure is the system; the translations are the workload.

This chapter is itself an example. The localization infrastructure described here is the same infrastructure that serves this very page in Japanese. The system describes itself.