Packages

ex_cldr

2.47.4
2.47.5 2.47.4 2.47.3 2.47.2 2.47.1 2.47.0 2.46.0 2.45.2 2.45.1 2.45.0 2.44.1 2.44.0 2.43.2 2.43.1 2.43.0 2.42.0 2.41.1 2.41.0 2.40.4 2.40.3 2.40.2 2.40.1 2.40.0 2.39.2 2.39.1 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 retired 2.37.2 2.37.1 2.37.0 2.36.0 2.35.1 2.35.0 retired 2.34.2 2.34.1 2.34.0 2.33.2 2.33.1 2.33.1-rc.0 2.33.0 2.32.1 2.32.0 2.31.0 2.30.0 2.29.0 2.28.0 2.27.1 2.27.0 retired 2.26.3 2.26.2 2.26.1 2.26.0 2.25.0 2.24.2 2.24.1 2.24.0 2.24.0-rc.6 2.24.0-rc.5 2.24.0-rc.4 2.24.0-rc.3 2.24.0-rc.2 2.24.0-rc.1 2.24.0-rc.0 2.23.2 2.23.1 2.23.0 2.22.1 2.22.0 2.21.0 2.20.0 2.20.0-rc.3 2.20.0-rc.2 2.20.0-rc.1 2.20.0-rc.0 2.19.1 2.19.0 2.18.2 2.18.1 2.18.0 2.18.0-rc.0 2.17.2 2.17.1 2.17.0 2.16.2 2.16.1 2.16.1-rc.0 2.16.0 2.15.0 2.14.1 2.14.0 2.14.0-rc.0 2.13.0 2.12.1 2.12.0 2.11.1 2.11.0 2.10.2 2.10.1 2.10.0 retired 2.9.0 2.8.1 2.8.0 2.7.2 2.7.1 2.7.0 2.6.2 2.6.1 2.6.0 2.5.0 2.4.3 2.4.2 2.4.1 2.4.0 2.3.2 2.3.1 2.3.0 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.0 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 2.0.0-rc.3 retired 2.0.0-rc.2 retired 2.0.0-rc.1 retired 2.0.0-rc.0 retired 1.8.2 1.8.1 1.8.0 1.7.1 1.7.0 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.2 1.5.1 1.5.0 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 retired 1.4.0-rc.3 retired 1.4.0-rc.2 retired 1.4.0-rc.1 retired 1.4.0-rc.0 retired 1.3.2 1.3.1 1.3.0 1.2.0 1.1.0 1.0.0 1.0.0-rc.3 retired 1.0.0-rc.2 retired 1.0.0-rc.1 retired 1.0.0-rc.0 retired 0.13.1 0.13.0 0.12.2 retired 0.12.1 retired 0.12.0 retired 0.11.0 retired 0.10.0 retired 0.9.0 retired 0.8.3 retired 0.8.2 retired 0.8.1 retired 0.8.0 retired 0.7.0 retired 0.6.2 retired 0.6.1 retired 0.6.0 retired 0.5.2 retired 0.5.1 retired 0.5.0 retired 0.4.2 retired 0.4.1 retired 0.4.0 retired 0.3.0 retired 0.2.1 retired 0.2.0 retired 0.1.3 retired 0.1.2 retired 0.1.1 retired 0.1.0 retired 0.0.20 retired 0.0.19 retired 0.0.18 retired 0.0.17 retired 0.0.16 retired 0.0.15 retired 0.0.14 retired 0.0.13 retired 0.0.12 retired 0.0.11 retired 0.0.10 retired 0.0.8 retired 0.0.7 retired 0.0.6 retired 0.0.5 retired 0.0.4 retired 0.0.3 retired

Common Locale Data Repository (CLDR) functions for Elixir to localize and format numbers, dates, lists, messages, languages, territories and units with support for over 700 locales for internationalized (i18n) and localized (L10N) applications.

Current section

Files

Jump to
ex_cldr lib cldr locale match.ex
Raw

lib/cldr/locale/match.ex

defmodule Cldr.Locale.Match do
@moduledoc """
Implements the [CLDR language matching algorithm](https://www.unicode.org/reports/tr35/tr35.html#LanguageMatching).
"""
alias Cldr.Locale
# Since the default distance for differnt territories is 4
# and the default distance for different scripts is 50 this
# default will match if there is a territory difference or
# a script different but not both. Bote that a language
# default distance is 80 so by default different languages
# can never match.
# UPDATED: now matching if only the language matches. Was 50,
# now 54.
@default_threshold 54
# When looking for a best match amongst multiple desired
# languages we want the languages earlier in the list to
# be preferred over those later in the list in the cases
# where their match distance is the same. We do that by
# demoting later entries in the list by am amount greater
# than the default territory difference (which is 4).
@more_than_territory_difference 5
@language_matching Cldr.Config.language_matching()
@paradigm_locales Map.fetch!(@language_matching, :paradigm_locales)
# Kept as @doc false accessors for backward compatibility with any
# downstream callers. The matching engine itself no longer iterates
# these — see Cldr.Locale.DistanceTrie.
@doc false
def language_matches do
Map.fetch!(@language_matching, :language_match)
end
@doc false
def match_variables do
Map.fetch!(@language_matching, :match_variables)
end
@doc false
def paradigm_locales do
@paradigm_locales
end
@doc false
def default_threshold do
@default_threshold
end
@doc """
Find the desired locale that is the best suported match.
### Arguments
* `desired` is any valid locale name or list of locale names
returned by `Cldr.known_locale_names/1` or a string or atom locale name.
* `options` is a keyword list of options.
### Options
* `:backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.
* `:supported` is a list of locale names that are supported by the application.
The default is `Cldr.known_locale_names/1`.
* `:threshold` filters the returned list to those locales that score below this
limit. The default is #{@default_threshold}.
### Returns
* A possibly empty list of `{supported_locale, numeric_score}` tuples sorted in ascending numeric
score order. The head of the list is considered the best match for `desired` in
`supported`.
### Examples
iex> Cldr.Locale.Match.best_match "zh-HK",
...> supported: ["zh", "zh-Hans", "zh-Hant", "en", "fr", "en-Hant"]
{:ok, "zh-Hant", 5}
iex> supported = Cldr.known_gettext_locale_names()
["bg_BG", "en", "en_GB", "es", "it"]
iex> Cldr.Locale.Match.best_match("en-GB", supported: supported)
{:ok, "en_GB", 0}
iex> Cldr.Locale.Match.best_match("zh-HK", supported: supported)
{:error, {Cldr.NoMatchingLocale, "No match for desired locales \\"zh-HK\\""}}
"""
@doc since: "2.44.0"
def best_match(desired, options \\ [])
def best_match(desired, options) do
threshold = Keyword.get(options, :threshold, @default_threshold)
backend = Keyword.get_lazy(options, :backend, &Cldr.default_backend!/0)
desired_list =
desired
|> List.wrap()
|> Enum.with_index()
supported =
options
|> Keyword.get_lazy(:supported, &backend.known_locale_names/0)
|> Enum.with_index()
matches =
for {candidate, priority} <- desired_list, {supported, index} <- supported,
{:ok, candidate_tag} = validate(candidate, backend, :skip_subtags_for_und),
{:ok, supported_tag} = validate(supported, backend),
match_distance = match_distance(candidate_tag, supported_tag, backend),
match_distance <= threshold do
{supported, supported_tag, match_distance, priority, index}
end
|> Enum.sort(&language_comparator/2)
# |> IO.inspect(label: "Ordered matches")
case matches do
[{supported, _supported_tag, distance, _priority, _index} | _rest] -> {:ok, supported, distance}
[] -> {:error, {Cldr.NoMatchingLocale, "No match for desired locales #{inspect desired}"}}
end
end
# "und" sorts after any other language that has the same distance score
defp language_comparator({"und", _, distance, _, _}, {_lang, _, distance, _, _}) do
false
end
defp language_comparator({_lang, _, distance, _, _}, {"und", _, distance, _, _}) do
true
end
defp language_comparator(a, b) do
if same_base_language?(a, b) do
match_key_with_paradigm(a) < match_key_with_paradigm(b)
else
match_key_no_paradigm(a) < match_key_no_paradigm(b)
end
end
defp same_base_language?(a, b) do
extract_base_language(a) == extract_base_language(b)
end
defp extract_base_language({_supported, supported_tag, _, _, _}) do
supported_tag.language
end
# If the language is a paradigmn locale then it
# takes precedence of non-paradigm locales. We
# leverage erlang term ordering to craft a match
# key. Since false sorts before true, we use
# "not in" rather than "in".
defp match_key_with_paradigm({_language, supported_tag, distance, priority, index}) do
maybe_paradigm_locale =
Locale.locale_name_from(supported_tag)
{distance + (priority * @more_than_territory_difference),
!paradigm_locale(maybe_paradigm_locale), index}
end
defp match_key_no_paradigm({_language, _supported_tag, distance, priority, index}) do
{distance + (priority * @more_than_territory_difference), index}
end
defp paradigm_locale(language) when is_binary(language) do
language in paradigm_locales()
end
@doc """
Return a match distance between a desired locale and
a supported locale.
### Arguments
* `desired` is any valid locale returned by `Cldr.known_locale_names/1`
or a string or atom locale name.
* `supported` is any valid locale returned by `Cldr.known_locale_names/1`
or a string or atom locale name.
* `backend` is any module that includes `use Cldr` and therefore
is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.
### Returns
* A numeric score indicating how well the supported locale can represent
the desired locale. A smaller number is better with a value under 10 being
a good fit and a number over 50 being a poor fit.
### Example
iex> Cldr.Locale.Match.match_distance("en", "en")
0
iex> Cldr.Locale.Match.match_distance("en-AU", "en")
5
iex> Cldr.Locale.Match.match_distance("en-AU", "en-GB")
3
iex> Cldr.Locale.Match.match_distance("zh-HK", "zh-Hant")
5
iex> Cldr.Locale.Match.match_distance("en", "zh-Hans")
134
"""
@doc since: "2.44.0"
def match_distance(desired, supported, backend \\ Cldr.default_backend!()) do
with {:ok, desired} <- validate(desired, backend, :skip_subtags_for_und),
{:ok, supported} <- validate(supported, backend) do
Cldr.Locale.DistanceTrie.lookup(
desired.language,
desired.script,
desired.territory,
supported.language,
supported.script,
supported.territory
)
end
end
@doc false
def validate(locale, backend, subtags \\ :add_subtags)
def validate(%Cldr.LanguageTag{} = locale, _backend, _add_subtags) do
{:ok, locale}
end
# Likely subtags has an entry for "und" that will
# transform it to `en-Latn-US` which is not what
# we want for matching. So don't apply likely subtags.
def validate("und" <> _rest = locale, backend, :skip_subtags_for_und) do
options = [skip_gettext_and_cldr: true, skip_rbnf_name: true, add_likely_subtags: false]
Cldr.Locale.canonical_language_tag(locale, backend, options)
end
# Fast path: when the locale is a precomputed known name on the backend,
# delegate to backend.validate_locale/1 which returns a fully maximised
# tag in O(1). This is the asymptotic fix — without it, best_match did
# full canonicalisation per supported locale on every call.
#
# Crucially, we MUST NOT fall through to backend.validate_locale/1 for
# unknown names: its generic clause calls Cldr.Locale.canonical_language_tag
# without skip_gettext_and_cldr, which re-enters Match.best_match and
# recurses indefinitely while best_match is itself iterating the
# supported list. Detect the precomputed path explicitly and use the
# safe slow path otherwise.
def validate(locale, backend, _add_subtags) when is_binary(locale) or is_atom(locale) do
cond do
not (Code.ensure_loaded?(backend) and function_exported?(backend, :validate_locale, 1)) ->
slow_validate(locale, backend)
precomputed?(locale, backend) ->
backend.validate_locale(locale)
true ->
slow_validate(locale, backend)
end
end
defp precomputed?(locale, backend) do
not is_nil(Cldr.known_locale_name(normalise_for_lookup(locale), backend))
end
defp normalise_for_lookup(locale) when is_atom(locale) do
locale
|> Atom.to_string()
|> normalise_for_lookup()
end
defp normalise_for_lookup(locale) when is_binary(locale) do
normalised =
locale
|> String.downcase()
|> Cldr.Config.locale_name_from_posix()
try do
String.to_existing_atom(normalised)
rescue
ArgumentError -> nil
end
end
defp slow_validate(locale, backend) do
options = [skip_gettext_and_cldr: true, skip_rbnf_name: true]
Cldr.Locale.canonical_language_tag(locale, backend, options)
end
end