Parse and resolve BCP 47 language tags.
This module lets you match a requested locale (or a prioritized list of
requested locales, as in an HTTP Accept-Language header) against the set of
locales you actually have translations for — without hard-coding which locales
exist. New translations can be added as data at runtime and resolution keeps
working.
The en locale — a sensible total default when no locale has been
negotiated yet, so callers can hold a concrete Locale (and thus a concrete
Bundle) without threading a Maybe everywhere.
toString default
--> "en"Parse a BCP 47 tag such as "en", "en-US", "zh-Hant-TW", or
"sr-Latn-RS". Both - and _ are accepted as separators. Returns Nothing
if there is no valid primary language subtag.
fromString "en-US" |> Maybe.map toString
--> Just "en-US"
fromString "ZH_hant_tw" |> Maybe.map toString
--> Just "zh-Hant-TW"The fallback chain for a locale, most specific first, following RFC 4647 "lookup" truncation: trailing subtags are dropped one group at a time down to the bare language.
fromString "zh-Hant-TW"
|> Maybe.map (fallbacks >> List.map toString)
--> Just [ "zh-Hant-TW", "zh-Hant", "zh" ]Find the best available locale for a single requested locale, using the
fallbacks chain. The requested locale is progressively
generalized until one of the available locales matches exactly (in canonical
form); returns Nothing if nothing matches.
resolve want =
[ "en", "en-GB", "fr" ]
|> List.filterMap fromString
|> (\avail -> fromString want |> Maybe.andThen (bestMatch avail))
|> Maybe.map toString
resolve "en-US" --> Just "en"
resolve "en-GB" --> Just "en-GB"
resolve "de" --> NothingNegotiate against a prioritized list of requested locales (highest priority
first, as in an Accept-Language header). Returns the bestMatch
for the first requested locale that resolves.
let
avail =
List.filterMap fromString [ "en", "fr-CA", "fr" ]
wanted =
List.filterMap fromString [ "de", "fr-FR", "en" ]
in
negotiate avail wanted |> Maybe.map toString
--> Just "fr"
A parsed, canonicalized BCP 47 language tag.
Canonical case is applied on parse: language lowercase (
en), script in title case (Latn), region uppercase (US), variants lowercase. Extension and private-use subtags (-u-…,-x-…) are not retained — they don't affect translation lookup.