Locale resolution
Real-world locale strings are messy: en-US, En_us, en, EN. localize-react is forgiving β it tries multiple normalizations before giving up.
Algorithmβ
Given <LocalizationProvider locale={input} translations={tree}>:
- Direct match. Is
tree[input]an object? Use it. - Normalize. Lowercase the input, replace
-with_. Try again. - Strip region. Take the leading segment (
en_usβen). Try again. - No match. Log a warning, render the empty map (every descriptor becomes its own fallback).
sanitizeLocale('en-US', { en: {...}, fr: {...} }); // β 'en' (step 3)
sanitizeLocale('EN_US', { en: {...} }); // β 'en'
sanitizeLocale('zh-Hant', { zh_hant: {...} }); // β 'zh_hant' (step 2)
sanitizeLocale('klingon', { en: {...} }); // β 'klingon' + warn
In practiceβ
- Pass whatever your auth/cookie/i18n routing layer gives you β no need to normalize beforehand.
- Name your translation keys however you prefer (
en,en_us,en_US); the lookup will find them. - If you mix conventions (
enanden_us), the most-specific match wins.
When nothing matchesβ
You get a console.warn:
[LOCALIZE-REACT]: There are no translations for specified locale klingon
β¦and translate(...) returns the descriptor (or defaultMessage) for every call. The app keeps rendering. Production users still see usable English-as-fallback strings rather than a blank page.
Why no locale negotiation?β
We deliberately don't ship full BCP 47 negotiation (CLDR matching, parent-locale chains). Two reasons:
- Size. Locale negotiation tables are big.
- Ambiguity. If your
en-USusers should fall back toen, you can shipenand let the simple normalizer handle it. If they should fall back to a different dialect, you know your product better than a generic matcher β pick the right key explicitly.
If you need richer matching, Intl.Locale and @formatjs/intl-locale-matcher are excellent and orthogonal to this library.