All lessons

Lesson 09·Unit 3 · The Intl toolbox·Locale matching & fallback chains·Lv 201·~8 min

Locale fallback & negotiation

Your bundle has pt-PT. A user lands from São Paulo with Accept-Language pt-BR. What language do they get? It depends on which of three negotiation algorithms you picked, and whether you knew you'd picked one at all.

BCP-47RFC 4647Accept-LanguageLocaleMatcher

By the end

  • ·Trace RFC 4647 lookup by hand: pt-BR to pt to your default, truncating from the right and never stepping sideways.
  • ·Explain why script beats region when matching Chinese, and why your zh bundles need Hans or Hant on them.
  • ·Read the q-values in an Accept-Language header, and say what they feed into and what they never promise.

The problem

Locale fallback ("we don't have your locale; here's our best guess") sounds simple, until you realize there are three RFC-defined algorithms (Basic, Lookup, Best Fit), they produce different answers, and the default JavaScript behaviour differs from the default HTTP Accept-Language behaviour. A Portuguese user from Brazil might see French because nobody specified u-rg- region preference. Worse: the bug is invisible to anyone testing in their own locale.

How it works

What the runtime does with pt-BR

A locale tag is a path, not a label. pt-BR decomposes into language (pt), region (BR), optionally script and variants in between. RFC 4647 lookup resolves a request by walking that path from the right: try pt-BR, then pt, then fall back to the default. The walk never steps sideways. pt-PT may look adjacent to a human, but to lookup it is not on the path.

That is by design. Lookup answers a narrow question: whether you have something the user asked for. Best-fit matching answers the friendlier question of the least-bad alternative. It is a different algorithm that scores distances between locales (CLDR ships the distance data) and can match pt-BR to pt-PT, or zh-Hant-HK to zh-Hant-TW. Browsers and libraries implement best-fit differently. That is why two stacks can negotiate different answers from identical inputs.

Three practical consequences follow. Ship a region-neutral base catalog (pt) whenever you ship any regional variant: it is the cheap net under every sibling region. Tag Chinese with scripts (zh-Hans, zh-Hant), because script outranks region in every sane match. When the user explicitly picks a language, store it: the stored choice should beat both algorithms forever.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  1. 1
    Load the scenario “Brazilian user, bundle has only pt-PT” and follow the fallback walk. pt-BR → pt → root → en. The user gets English while Portuguese sits in the bundle: lookup truncates the request tag and never “reaches across” to a sibling region.
  2. 2
    Now add pt to the bundle field (keep pt-PT too). The walk now catches at the base tag: pt-BR users get pt. Shipping a region-neutral base catalog is the cheap fix for sibling-region traffic.
  3. 3
    Load “Hong Kong user, bundle has zh-Hans + zh-Hant”. zh-Hant-HK matches zh-Hant-TW: the script subtag (Hant) outranks region. Chinese negotiation without script tags is guaranteed wrong for someone.
  4. 4
    Load “Swiss user” and read all three algorithm columns. Lookup, best-fit, and Accept-Language q-values can each pick a different winner from the same inputs: “what locale does the user get” has three defensible answers.

Background: Classic: the bundle ships European Portuguese, the user is in São Paulo. Best-fit matches them. Pure lookup walks pt-BR → pt → root, which falls through to en.

The fallback walk, per user-preferred tag

  1. pt-BRpreference #1
    pt-BRpt→ root → ultimate fallback
  2. ptpreference #2
    pt→ root → ultimate fallback
  3. enpreference #3PICKED
    en

Negotiation result

User gets en (matched from preference en). In a soft mismatch (de-AT user, de-DE bundle), users will mostly not notice. But legal copy, currency, and date format may not match their region. In a hard mismatch (a Brazilian user who gets European Portuguese), the entire copy register is wrong: pt-PT reads to Brazilians the way "colour" and "lorry" read to Americans, on every screen.

Three negotiation algorithms, one input

AlgorithmSpecBehaviour
Lookup deterministicRFC 4647 §3.4Strips subtags from the right (zh-Hant-HK → zh-Hant → zh) until a bundle entry matches. Predictable, portable, and the default in most i18n libraries.
Best-fit heuristicIntl ECMA-402Engine-defined. V8 uses ICU's LocaleMatcher with CLDR's languageMatching.xml, so it knows that pt-BR ↔ pt-PT is a reasonable swap and en ↔ ja is not.
Accept-Language q-vals HTTP-onlyRFC 9110 §12.5.4The browser sends q=0.9, q=0.5 weights. Your server picks the highest-weighted available locale. Most CDNs do this. Most SPAs ignore it and use navigator.languages instead.

If you remember one thing

Locale negotiation is an algorithm you choose. Know whether you use RFC 4647 lookup, best-fit matching, or Accept-Language parsing. The three give different answers from the same inputs.

What to do about it

  • Use @formatjs/intl-localematcher on the server to negotiate locale from Accept-Language against your supported bundle list. Never trust the raw header.
  • Document your fallback chain explicitly: pt-BR → pt → en. Stakeholders need to know what language a user from an unsupported region will see.
  • For BCP-47 region preferences (en-US-u-rg-gbzzzz means "English in US, but use UK formats"), surface this in account settings. Power users in dual-locale countries (Canada, Belgium, Switzerland) will thank you.
  • Test the negotiation with at least one locale that's not in your bundle; your fallback path is more important than your primary path.

Use this with

Stakeholders

EngineeringProduct

Moments

  • ·Adding a new locale to the bundle
  • ·Server-side rendering setup
  • ·Debugging 'why does this user see French?'

Field note

BCP-47 even has a region code for “Spanish, but not Spain's”: es-419, Latin America, borrowed from the UN M.49 area code 419. It exists because twenty per-country Spanish catalogs do not scale, and because es-ES vocabulary reads foreign across an ocean. Spain's ordenador is Latin America's computadora, and its coche their carro, on every screen.

W3C: Choosing a language tag

Quick check

3 questions · pass at 2+

  1. Question 1/3

    Your bundle has [en, pt-PT]. A user requests pt-BR. What does strict RFC 4647 lookup serve them?

  2. Question 2/3

    A zh-Hant-HK user, bundle [zh-Hans-CN, zh-Hant-TW]. Which is the better match and why?

  3. Question 3/3

    In the header Accept-Language: de-CH, de;q=0.9, en;q=0.8, what does q mean?

Words you'll hear

Where to read more

Related lessons