LocaleSlug.Resolver (LocaleSlug v0.2.0)

Copy Markdown View Source

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 -> neutral

with 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

Types

Why a tag was rejected. Not the same as 'we have no table for it'.

t()

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

invalid_reason()

@type invalid_reason() :: :country_only | :not_a_language_tag

Why a tag was rejected. Not the same as 'we have no table for it'.

t()

@type t() :: %LocaleSlug.Resolver{
  lowercase_mode: :default | :turkic | :greek,
  schemas: %{optional(atom()) => String.t()},
  table: map() | nil,
  tag: String.t() | nil
}

Functions

default_schema(script)

@spec default_schema(atom()) :: String.t() | nil

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.

known_schemas()

@spec known_schemas() :: [String.t()]

Schema ids that ship, for the :schema option's value space.

named_schemas()

@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.

neutral()

@spec neutral() :: t()

A resolution with no L2 table and every script on its default schema.

resolve(tag)

@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"