defmodule Localize.Inputs.Number.Symbols do
@moduledoc """
Locale-derived display data for number form inputs.
Returns the decimal/grouping separator characters, the active
number system, and the locale's minus sign — everything a JS
hook needs to render a number in the user's locale.
All locale lookups go through `Localize.validate_locale/1`;
the resulting `t:Localize.LanguageTag.t/0`'s
`:cldr_locale_id` is the canonical id reported back. Number
system resolution goes through
`Localize.Number.System.number_system_from_locale/1` so
Arabic-Indic, Persian, and other non-Latin digit systems work
out of the box.
"""
alias Localize.Inputs.Number.NoNumberSymbolsError
alias Localize.LanguageTag
alias Localize.Number.Symbol
alias Localize.Number.System
@typedoc """
Locale display data resolved by `number_for_locale/1`.
* `:locale` — the canonical CLDR locale id (atom).
* `:language_tag` — the full `t:Localize.LanguageTag.t/0`.
* `:number_system` — the active number system (`:latn`,
`:arab`, `:arabext`, …).
* `:decimal` — the locale's decimal separator character.
* `:group` — the locale's grouping separator character.
* `:minus_sign` — the locale's minus sign character.
"""
@type t :: %__MODULE__{
locale: atom(),
language_tag: LanguageTag.t(),
number_system: atom(),
decimal: String.t(),
group: String.t(),
minus_sign: String.t()
}
defstruct [
:locale,
:language_tag,
:number_system,
:decimal,
:group,
:minus_sign
]
@doc """
Resolves display data for the given locale.
### Arguments
* `locale` is a locale identifier (atom, string, or
`t:Localize.LanguageTag.t/0`). Defaults to
`Localize.get_locale/0`.
### Returns
* `{:ok, t()}` on success.
* `{:error, Exception.t()}` when the locale can't be parsed
(`Localize.InvalidLocaleError`), the number system can't be
resolved, or no symbols are available
(`Localize.Inputs.Number.NoNumberSymbolsError`).
### Examples
iex> {:ok, info} = Localize.Inputs.Number.Symbols.number_for_locale(:en)
iex> {info.decimal, info.group, info.number_system}
{".", ",", :latn}
iex> {:ok, info} = Localize.Inputs.Number.Symbols.number_for_locale(:de)
iex> {info.decimal, info.group}
{",", "."}
"""
@spec number_for_locale(LanguageTag.t() | atom() | String.t() | nil) ::
{:ok, t()} | {:error, Exception.t()}
def number_for_locale(locale \\ nil) do
with {:ok, language_tag} <- Localize.validate_locale(locale || Localize.get_locale()),
{:ok, number_system} <- System.number_system_from_locale(language_tag),
{:ok, symbols} <- resolve_symbols(language_tag, number_system) do
{:ok,
%__MODULE__{
locale: language_tag.cldr_locale_id,
language_tag: language_tag,
number_system: number_system,
decimal: symbol_string(symbols.decimal),
group: symbol_string(symbols.group),
minus_sign: symbol_string(symbols.minus_sign)
}}
end
end
# ── internal ────────────────────────────────────────────────
defp resolve_symbols(language_tag, number_system) do
case Symbol.number_symbols_for(language_tag, number_system) do
{:ok, symbols} ->
{:ok, symbols}
{:error, _} ->
# CLDR doesn't always populate symbols for every
# (locale, system) pair. Fall back to the locale's
# *default* number system — the value at the `:default`
# key of `Localize.Number.System.number_systems_for/1`
# (e.g. `:latn` for `en`, `:arabext` for `fa`).
with {:ok, %{default: default_system}} <- System.number_systems_for(language_tag),
true <- default_system != number_system,
{:ok, symbols} <- Symbol.number_symbols_for(language_tag, default_system) do
{:ok, symbols}
else
_ ->
{:error,
NoNumberSymbolsError.exception(
locale: language_tag.cldr_locale_id,
number_system: number_system
)}
end
end
end
# Number-symbol fields are either a plain binary or a map of
# variants keyed by `:standard` / `:accounting` / etc. Every
# locale CLDR ships has one of these two shapes (verified
# empirically across ~70 locales). If a new shape ever appears
# we want a loud `FunctionClauseError` so it surfaces rather
# than silently returning garbage from `Map.values/1`.
defp symbol_string(value) when is_binary(value), do: value
defp symbol_string(%{standard: value}) when is_binary(value), do: value
end