lib/cldr/locale_display.ex

defmodule Cldr.LocaleDisplay do
  @moduledoc """
  Implements the [CLDR locale display name algorithm](https://unicode-org.github.io/cldr/ldml/tr35-general.html#locale_display_name_algorithm) to format
  a `t:Cldr.LanguageTag` structs for presentation uses.

  """

  @doc false
  def cldr_backend_provider(config) do
    Cldr.LocaleDisplay.Backend.define_locale_display_module(config)
  end

  import Cldr.LanguageTag, only: [empty?: 1]

  alias Cldr.LanguageTag
  alias Cldr.Locale

  @basic_tag_order [:language, :script, :territory, :language_variants]
  @extension_order [:transform, :locale, :extensions]
  @omit_script_if_only_one false

  @type display_options :: [
          {:language_display, :standard | :dialect},
          {:prefer, atom()},
          {:locale, Cldr.Locale.locale_name() | Cldr.LanguageTag.t()},
          {:backend, Cldr.backend()}
        ]

  @doc """
  Returns a localised display name for a
  locale.

  UI applications often have a requirement
  to present locale choices to an end user.

  This function takes a `t.Cldr.LanguageTag`
  and using the [CLDR locale display name algorithm](https://unicode-org.github.io/cldr/ldml/tr35-general.html#locale_display_name_algorithm)
  produces a string suitable for presentation.

  ### Arguments

  * `language_tag` is any `t:Cldr.LanguageTag` or
    a locale name as an atom or string.

  * `options` is a keyword list of options.

  ### Options

  * `:language_display` determines if a language
    is displayed in `:standard` format (the default)
    or `:dialect` format.

  * `:prefer` signals the preferred name for
    a subtag when there are alternatives.
    The default is `:standard`. Few subtags
    provide alternative renderings. Some of
    the alternative preferences are `:short`,
    `:long`, `:menu` and `:variant`.

  * `:locale` is a `t:Cldr.LanguageTag` or any valid
    locale name returned by `Cldr.known_locale_names/1`.

  * `:backend` is any module that includes `use Cldr` and therefore
    is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.

  ### Returns

  * `{:ok, string}` representing a name
    suitable for presentation purposes or

  * `{:error, {exception, reason}}`

  ### Notes

  * The difference between `language_display: :standard` and
    `:dialect` is related to how compound languages are displayed.
    See the examples for "nl-BE" below.

  ### Examples

      iex> Cldr.LocaleDisplay.display_name("en")
      {:ok, "English"}

      iex> Cldr.LocaleDisplay.display_name("en-US", language_display: :standard)
      {:ok, "English (United States)"}

      iex> Cldr.LocaleDisplay.display_name("en-US", language_display: :dialect)
      {:ok, "American English"}

      iex> Cldr.LocaleDisplay.display_name("en-US-u-ca-gregory-cu-aud", language_display: :dialect)
      {:ok, "American English (Gregorian Calendar, Currency: A$)"}

      iex> Cldr.LocaleDisplay.display_name("en-US-u-ca-gregory-cu-aud", locale: "fr", language_display: :dialect)
      {:ok, "anglais américain (calendrier grégorien, devise : A$)"}

      iex> Cldr.LocaleDisplay.display_name("nl-BE")
      {:ok, "Dutch (Belgium)"}

      iex> Cldr.LocaleDisplay.display_name("nl-BE", language_display: :dialect)
      {:ok, "Flemish"}

  """
  @spec display_name(Locale.locale_reference(), display_options()) ::
          {:ok, String.t()} | {:error, {module(), String.t()}}

  def display_name(language_tag, options \\ [])

  def display_name(language_tag, options) when is_binary(language_tag) or is_atom(language_tag) do
    {_in_locale, backend} = Cldr.locale_and_backend_from(options)
    options = Keyword.put_new(options, :add_likely_subtags, false)

    with {:ok, locale} <- Cldr.Locale.canonical_language_tag(language_tag, backend, options) do
      display_name(locale, options)
    end
  end

  def display_name(%LanguageTag{} = language_tag, options) do
    {in_locale, backend} = Cldr.locale_and_backend_from(options)
    display_backend = Module.concat(language_tag.backend, :LocaleDisplay)
    options = Keyword.merge(default_options(), options)

    standard_or_dialect = Keyword.get(options, :language_display)
    prefer = Keyword.get(options, :prefer)

    # FIXME Catering for legacy
    prefer =  if prefer == :default, do: :standard, else: prefer

    with {:ok, in_locale} <- Cldr.validate_locale(in_locale, backend),
         {:ok, display_names} <- display_backend.display_names(in_locale),
         {:ok, matched_tags, language_name} <- language_name(language_tag, display_names, prefer, standard_or_dialect) do
      language_tag =
        merge_extensions_and_private_use(language_tag)

      subtag_names =
        language_tag
        |> subtag_names(@basic_tag_order -- matched_tags, prefer, display_names)
        |> List.flatten()
        |> Enum.map(&replace_parens_with_brackets/1)
        |> join_subtags(display_names)

      options =
        Keyword.put(options, :locale, in_locale)

      extension_names =
        @extension_order
        |> Enum.map(&Cldr.DisplayName.display_name(Map.fetch!(language_tag, &1), options))
        |> Enum.reject(&empty?/1)
        |> join_subtags(display_names)

      {:ok, format_display_name(language_name, subtag_names, extension_names, display_names)}
    end
  end

  defp language_name(language_tag, display_names, prefer, standard_or_dialect) do
    match_fun =  &language_match_fun(&1, &2, :language, prefer, display_names)

    case first_match(language_tag, match_fun, @omit_script_if_only_one, standard_or_dialect) do
      {matched_tags, language_name} -> {:ok, matched_tags, language_name}
      nil -> {:error, {Cldr.DisplayName.NoDataError, "No locale display data for #{inspect language_tag}"}}
    end
  end

  defp default_options do
    [
      prefer: :standard,
      add_likely_subtags: false,
      language_display: :standard,
      prefer: :standard
    ]
  end

  @doc """
  Returns a localised display name for a
  locale.

  UI applications often have a requirement
  to present locale choices to an end user.

  This function takes a `t.Cldr.LanguageTag`
  and using the [CLDR locale display name algorithm](https://unicode-org.github.io/cldr/ldml/tr35-general.html#locale_display_name_algorithm)
  produces a string suitable for presentation.

  ### Arguments

  * `language_tag` is any `t:Cldr.LanguageTag` or
    a locale name as an atom or string.

  * `options` is a keyword list of options.

  ### Options

  * `:language_display` determines if a language
    is displayed in `:standard` format (the default)
    or `:dialect` format.

  * `:prefer` signals the preferred name for
    a subtag when there are alternatives.
    The default is `:standard`. Few subtags
    provide alternative renderings. Some of
    the alternative preferences are `:short`,
    `:long`, `:menu` and `:variant`.

  * `:locale` is a `t:Cldr.LanguageTag` or any valid
    locale name returned by `Cldr.known_locale_names/1`.

  * `:backend` is any module that includes `use Cldr` and therefore
    is a `Cldr` backend module. The default is `Cldr.default_backend!/0`.

  ### Returns

  * a string representation of the language tag
    suitable for presentation purposes or

  * raises an exception.

  ### Notes

  * The difference between `language_display: :standard` and
    `:dialect` is related to how compound languages are displayed.
    See the examples for "nl-BE" below.

  ### Examples

      iex> Cldr.LocaleDisplay.display_name!("en")
      "English"

      iex> Cldr.LocaleDisplay.display_name!("en-US", language_display: :dialect)
      "American English"

      iex> Cldr.LocaleDisplay.display_name!("en-US")
      "English (United States)"

      iex> Cldr.LocaleDisplay.display_name!("en-US-u-ca-gregory-cu-aud", language_display: :dialect)
      "American English (Gregorian Calendar, Currency: A$)"

      iex> Cldr.LocaleDisplay.display_name!("en-US-u-ca-gregory-cu-aud", locale: "fr", language_display: :dialect)
      "anglais américain (calendrier grégorien, devise : A$)"

  """
  @spec display_name!(Locale.locale_reference(), display_options()) ::
          String.t() | no_return()

  def display_name!(language_tag, options \\ []) do
    case display_name(language_tag, options) do
      {:ok, locale} -> locale
      {:error, {exception, reason}} -> raise exception, reason
    end
  end

  defp merge_extensions_and_private_use(%LanguageTag{private_use: []} = language_tag) do
    language_tag
  end

  defp merge_extensions_and_private_use(%LanguageTag{} = language_tag) do
    extensions = Map.put_new(language_tag.extensions, "x", language_tag.private_use)
    Map.put(language_tag, :extensions, extensions)
  end

  # If matching on the compound locale then we
  # don't need to take any action
  defp first_match(language_tag, match_fun, omit_script_if_only_one?, :dialect) do
    Cldr.Locale.first_match(language_tag, match_fun, omit_script_if_only_one?)
  end

  # If we don't want a compound language then we need to omit
  # the territory when matching but restore it afterwards so
  # its generated as a subtag
  @reinstate_subtags [:territory, :script]

  defp first_match(language_tag, match_fun, omit_script_if_only_one?, :standard) do
    language_tag =
      Enum.reduce(@reinstate_subtags, language_tag, fn key, tag ->
        Map.put(tag, key, nil)
      end)

    case Cldr.Locale.first_match(language_tag, match_fun, omit_script_if_only_one?) do
      {matched_tags, display_name} ->
        {matched_tags -- @reinstate_subtags, display_name}

      nil ->
        nil
    end
  end

  # When prefer: :menu we may have either a map with :core and :extension
  # in which case `:core is the language name and :extension because the
  # head of the extensions.
  defp format_display_name(%{core: core, extension: extension}, subtag_names, extension_names, display_names) do
    format_display_name(core, [extension | subtag_names], extension_names, display_names)
  end

  # prefer: :menu might also have a single :alt form (gradually being replaced by
  # the :core/:extension form).  The dprecated :core form (with no :extension) is a
  # data bug in ex_cldr to be fixed in ex_cldr version 2.44.1.

  defp format_display_name(%{alt: alt}, subtag_names, extension_names, display_names) do
    format_display_name(alt, subtag_names, extension_names, display_names)
  end

  defp format_display_name(language_name, [], [], _display_names) do
    replace_parens_with_brackets(language_name)
  end

  defp format_display_name(language_name, subtag_names, extension_names, display_names) do
    language_name = replace_parens_with_brackets(language_name)
    locale_pattern = get_in(display_names, [:locale_display_pattern, :locale_pattern])

    subtags =
      [subtag_names, extension_names]
      |> Enum.reject(&empty?/1)
      |> join_subtags(display_names)

    [language_name, subtags]
    |> Cldr.Substitution.substitute(locale_pattern)
    |> List.to_string()
  end

  defp subtag_names(_locale, [],  _prefer, _display_names) do
    []
  end

  defp subtag_names(locale, subtags, prefer, display_names) do
    subtags
    |> Enum.map(&get_display_name(locale, display_names, &1, prefer))
    |> Enum.reject(&empty?/1)
  end

  defp get_display_name(locale, display_names, subtag, prefer) do
    case Map.fetch!(locale, subtag) do
      [_ | _] = subtags ->
        Enum.map(subtags, fn value ->
          display_name = get_in(display_names, [subtag, value]) || value

          # The ICU test data does this. Its not great
          # but it matches the output from ICU.
          if display_name == "FONIPA", do: "fonipa", else: display_name
        end)
        |> Enum.sort()

      subtag_value ->
        get_in(display_names, [subtag, subtag_value]) || subtag_value
    end
    |> get_display_preference(prefer)
  end

  @doc false
  def get_display_preference(nil, _preference) do
    nil
  end

  def get_display_preference(value, _preference) when is_binary(value) do
    value
  end

  def get_display_preference(value, _preference) when is_atom(value) do
    to_string(value)
  end

  def get_display_preference(values, preference) when is_list(values) do
    Enum.map(values, &get_display_preference(&1, preference))
  end

  def get_display_preference(values, preference) when is_map(values) do
    Map.get(values, preference) || Map.fetch!(values, :standard)
  end

  defp join_subtags([], _display_names) do
    []
  end

  defp join_subtags([field], _display_names) do
    [field]
  end

  defp join_subtags(fields, display_names) do
    join_pattern = get_in(display_names, [:locale_display_pattern, :locale_separator])
    Enum.reduce(fields, &Cldr.Substitution.substitute([&2, &1], join_pattern))
  end

  defp language_match_fun(locale_name, matched_tags, field, prefer, display_names) do
    cond do
      display_name = get_in(display_names, [field, locale_name, prefer]) ->
        {matched_tags, display_name}

      display_name = get_in(display_names, [field, locale_name, :standard]) ->
        {matched_tags, display_name}

      true ->
        nil
    end
  end

  @doc false
  def replace_parens_with_brackets(value) when is_binary(value) do
    value
    |> String.replace("(", "[")
    |> String.replace(")", "]")
    |> String.replace("(", "[")
    |> String.replace(")", "]")
  end

  # Joins field values together using the
  # localised format

  @doc false
  def join_field_values([], _display_names) do
    []
  end

  def join_field_values(fields, display_names) do
    join_pattern = get_in(display_names, [:locale_display_pattern, :locale_separator])
    Enum.reduce(fields, &Cldr.Substitution.substitute([&2, &1], join_pattern))
  end

  defimpl Cldr.DisplayName, for: Cldr.LanguageTag do
    def display_name(language_tag, options) do
      Cldr.LocaleDisplay.display_name!(language_tag, options)
    end
  end

  defimpl Cldr.DisplayName, for: Map do
    def display_name(map, _options) when map == %{} do
      ""
    end

    def display_name(map, options) do
      Cldr.LocaleDisplay.Extension.display_name(map, options)
    end
  end
end