LangTags: IANA Language Tags for Elixir

Copy Markdown View Source

CI Hex.pm Docs

Work with IANA language tags in Elixir, based on BCP 47 (RFC 5646) and the IANA language subtag registry.

The registry is parsed at compile time into lookup tables embedded in the compiled module, so a lookup is a constant-time map access on a term that is already in memory: there is no runtime parsing, no ETS table, no process to supervise, and no runtime dependencies.

Installation

Add lang_tags to your dependencies in mix.exs:

def deps do
  [{:lang_tags, "~> 0.2"}]
end

Usage

Look up a subtag:

iex> LangTags.language("en")
%{"Record" => %{"Added" => "2005-10-16", "Description" => ["English"],
    "Subtag" => "en", "Suppress-Script" => "Latn", "Type" => "language"},
  "Subtag" => "en"}

Resolve a deprecated or grandfathered tag to its preferred value:

iex> LangTags.Tag.preferred("i-klingon")
%{"Tag" => "tlh"}

Format a tag according to the RFC 5646 case conventions:

iex> "az-latn-az" |> LangTags.Tag.new() |> LangTags.Tag.format()
"az-Latn-AZ"

Validate a tag, and find out why it was rejected:

iex> LangTags.Tag.valid?("en-GB")
true

iex> LangTags.Tag.valid?("gsw-Latn")
false

iex> LangTags.Tag.errors("gsw-Latn")
[%{code: :suppress_script, subtag: "latn",
   message: "the script subtag 'latn' is the default for language 'gsw' and should be omitted"}]

errors/1 reports every problem it finds rather than stopping at the first, so a caller can show them together. See the documentation for the full list of codes.

Search tags and subtags by description, with exact matches first:

iex> LangTags.search("Maltese") |> Enum.map(&LangTags.SubTag.format/1)
["mt", "mdl", "mdl"]

Check which types a string is registered as:

iex> LangTags.types("xml")
["extlang", "language"]

Report the date of the bundled registry:

iex> LangTags.date()
"2026-08-08"

See the documentation for the full API.

Relationship to localize and ex_cldr

localize — and ex_cldr, the family it replaces — also parses RFC 5646 tags, so it is easy to confuse them with this library. They answer different questions.

localize resolves a tag to a locale it ships data for: it normalizes the tag, resolves it to a CLDR canonical form through likely-subtag resolution, and gives you the data to format numbers, dates, units, plurals and territory names for one of CLDR's ~766 locales. Localize.validate_locale/1 answers "can I localize with this?"

lang_tags validates a tag against the IANA registry: whether every subtag is actually registered, whether the tag or one of its subtags is deprecated and what its preferred value is, and whether it breaks a Suppress-Script or variant prefix rule — with a code and a message for each problem it finds. The two are not the same test: bis, in and zh-cmn-Hant all resolve to a locale under localize, while lang_tags reports them as unregistered or deprecated and hands you the replacement to store.

So the two compose: reject or repair the tag against the registry, then ask localize for a locale.

defmodule MyApp.Locale do
  @doc "Resolve an externally supplied language tag to a locale we can format with."
  def resolve(input) do
    tag = LangTags.Tag.new(input)

    case LangTags.Tag.errors(tag) do
      [] ->
        Localize.validate_locale(LangTags.Tag.format(tag))

      errors ->
        # A deprecated tag carries a modern replacement; anything else is
        # input we should not accept.
        case LangTags.Tag.preferred(tag) do
          nil -> {:error, errors}
          preferred -> Localize.validate_locale(LangTags.Tag.format(preferred))
        end
    end
  end
end
# Case-corrected, then resolved.
iex> MyApp.Locale.resolve("en-gb")
{:ok, Localize.LanguageTag.new!("en-GB")}

# Deprecated since 2009. Handing localize the preferred value "cmn-Hant"
# resolves to zh-Hant, where passing "zh-cmn-Hant" to localize directly leaves
# the legacy extlang form in place.
iex> MyApp.Locale.resolve("zh-cmn-Hant")
{:ok, Localize.LanguageTag.new!("zh-Hant")}

# Rejected, with a reason to show the caller.
iex> MyApp.Locale.resolve("en-Qqqq")
{:error, [%{code: :unknown, subtag: "qqqq", message: "'qqqq' is not registered"}]}

Reach for lang_tags alone when you accept, correct or store tags but never localize — an Accept-Language header, an xml:lang attribute, a locale column. Reach for localize alone when your locales are a fixed set you control.

Already on ex_cldr? The same split applies, with Cldr.validate_locale/2 in place of Localize.validate_locale/1. ex_cldr is superseded by localize; see its documentation for the current support timeline.

Updating the registry

The IANA registry changes over time. To refresh the bundled copy:

$ mix lang_tags.update
$ mix compile --force

The recompile is required because the registry is baked in at build time.

Use mix lang_tags.update --check to report whether an update is available without writing anything; it exits non-zero when one is, so it can drive a scheduled job.

Changelog

See CHANGELOG.md.

Javascript version

This project is an Elixir version of the language-tags Javascript project.

License

Apache License 2.0. See LICENSE.