BCP 47 tag handling: what is accepted, what is rejected, and what a tag resolves to.
"I don't support your language" is not an error. "That isn't a language" is.
A well-formed tag with no table is a silent miss — L0 and L1 still produce a correct slug, just not locale-tuned. A country code or a display name is a different thing: the caller asked a question we cannot answer, and answering a different question silently would be worse.
Only codes that are not valid ISO 639-1 language subtags are rejected (GB,
CZ, JP). If a string is a real language code we honour the documented contract
and read it as one — even when it doubles as a country code for a country that
speaks something else. ee is Ewe, not Estonia; ar is Arabic, not Argentina.
We ship no table for most of those, so they resolve to a correct neutral slug.
The fallback chain never crosses a script boundary
lang-Script-Region -> lang-Region -> lang-Script -> lang -> neutralwith one binding exception: when an explicit script subtag is present, a step whose
script differs is skipped. So zh-Hant resolves to neutral rather than falling
through to a Hans-biased bare zh.
Summary
Functions
The default L1 schema id for a script, or nil.
Schema ids that ship, for the :schema option's value space.
Every schema id this module can name — the script defaults plus the per-locale selections above.
A resolution with no L2 table and every script on its default schema.
Resolves a locale argument.
Types
@type invalid_reason() :: :country_only | :not_a_language_tag
Why a tag was rejected. Not the same as 'we have no table for it'.
Functions
The default L1 schema id for a script, or nil.
Exposed because it is also the backstop: LocaleSlug.layers_for/2 puts this table
underneath whichever schema the caller selected, so a national standard that keys
only its own alphabet does not delete the letters it omits.
@spec known_schemas() :: [String.t()]
Schema ids that ship, for the :schema option's value space.
@spec named_schemas() :: [String.t()]
Every schema id this module can name — the script defaults plus the per-locale selections above.
Exists so a test can prove they all ship. This list drifting ahead of priv/scripts/
IS the uk/bg/sr bug: uk pointed at kmu_2010, no such table existed, and the
mismatch was invisible until a Ukrainian slug came back empty.
@spec neutral() :: t()
A resolution with no L2 table and every script on its default schema.
@spec resolve(LocaleSlug.locale()) :: {:ok, t()} | {:missing, String.t(), t()} | {:invalid, invalid_reason()}
Resolves a locale argument.
{:ok, %Resolver{}} a table matched (possibly after fallback)
{:missing, normalized_tag, %Resolver{}} well-formed, no L2 table — but the
resolution still carries the lowercase
mode and schema defaults, which apply
{:invalid, :country_only} "GB"
{:invalid, :not_a_language_tag} "Estonian", "eesti"