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"}]
endUsage
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.