lib/cldr/language.ex

defmodule Cldr.Language do
  @moduledoc """
  Cldr Languages does provide functionality regarding languages within the data
  supplied by the Cldr core package.

  To use the functionality of this module you need to [register it as provider](https://github.com/kipcole9/cldr#providers) in your cldr backend module.

  ## Example

      defmodule MyApp.Backend do
        use Cldr,
          providers: [Cldr.Language, …],
          …
      end

  ## Interface

  All functionality will be compiled into `MyApp.Backend.Language`.

  ### Functions

  * `available_languages/0`
  * `available_languages/1`
  * `known_languages/0`
  * `known_languages/1`
  * `to_string/1`
  * `to_string/2`

  """

  # This is only meant to be called by Cldr
  @doc false
  def cldr_backend_provider(config) do
    module = inspect(__MODULE__)
    backend = config.backend
    config = Macro.escape(config)

    quote location: :keep,
          bind_quoted: [
            module: module,
            backend: backend,
            config: config
          ] do
      defmodule Language do
        alias Cldr.LanguageTag
        alias Cldr.Locale

        @type styles :: :standard | :short
        @styles [:standard, :short]

        # Simpler than unquoting the backend everywhere
        defp backend, do: unquote(backend)
        defp get_locale, do: backend().get_locale()
        defp default_locale, do: backend().default_locale()

        @doc """
        Return all the languages' iso-codes available for a given locale.
        Defaults to the current locale.

        ## Example

            > #{inspect(__MODULE__)}.Language.available_languages(:en)
            ["aa", "ab", "ace", "ach", "ada", "ady", "ae", "aeb", "af", "afh", "agq", "ain",
            "ak", "akk", "akz", "ale", "aln", "alt", "am", "an", "ang", "anp", "ar",
            "ar-001", "arc", "arn", "aro", "arp", "arq", "ars", "arw", "ary", "arz", "as",
            "asa", "ase", "ast", "av", "avk", "awa", "ay", "az", "ba", "bal", "ban", "bar",
            "bas", "bax", "bbc", "bbj", ...]
        """
        @spec available_languages() :: list(String.t()) | {:error, term()}
        @spec available_languages(Locale.locale_name() | LanguageTag.t()) ::
                list(String.t()) | {:error, term()}
        def available_languages(locale \\ get_locale())

        def available_languages(%LanguageTag{cldr_locale_name: cldr_locale_name}) do
          available_languages(cldr_locale_name)
        end

        @doc """
        Return a map of iso-code keyed maps of language names in any available
        formats for the given locale.
        Defaults to the current locale.

        ## Example

            > #{inspect(__MODULE__)}.Language.known_languages(:en)
            %{"bez" => %{standard: "Bena"}, "lo" => %{standard: "Lao"},
            "kha" => %{standard: "Khasi"}, "eo" => %{standard: "Esperanto"},
            "rm" => %{standard: "Romansh"}, "ja" => %{standard: "Japanese"},
            "sw-CD" => %{standard: "Congo Swahili"},
            "pdc" => %{standard: "Pennsylvania German"}, "om" => %{standard: "Oromo"},
            "jut" => %{standard: "Jutish"}, "lij" => %{standard: "Ligurian"},
            "kut" => %{standard: "Kutenai"}, "vep" => %{standard: "Veps"},
            "yao" => %{standard: "Yao"}, "gez" => %{standard: "Geez"},
            "cr" => %{standard: "Cree"}, "ne" => %{standard: "Nepali"},
            "zbl" => %{standard: "Blissymbols"}, "ae" => %{standard: "Avestan"},
            "rof" => %{standard: "Rombo"}, "tkl" => %{standard: "Tokelau"},
            "rgn" => %{standard: "Romagnol"}, "el" => %{standard: "Greek"},
            "myv" => %{standard: "Erzya"}, "smj" => %{standard: "Lule Sami"},
            "fo" => %{standard: "Faroese"}, "ii" => %{standard: "Sichuan Yi"},
            "bum" => %{standard: "Bulu"}, "za" => %{standard: "Zhuang"},
            "raj" => %{standard: "Rajasthani"}, "mrj" => %{standard: "Western Mari"},
            "stq" => %{standard: "Saterland Frisian"}, "hu" => %{standard: "Hungarian"},
            "mga" => %{standard: "Middle Irish"}, "bej" => %{standard: "Beja"},
            "yue" => %{standard: "Cantonese"}, "xog" => %{standard: "Soga"},
            "ttt" => %{standard: "Muslim Tat"}, "uga" => %{standard: "Ugaritic"},
            "rup" => %{standard: "Aromanian"},
            "crs" => %{standard: "Seselwa Creole French"}, "oc" => %{standard: "Occitan"},
            "chp" => %{standard: "Chipewyan"}, "zen" => %{standard: "Zenaga"},
            "kmb" => %{standard: "Kimbundu"}, "nr" => %{standard: "South Ndebele"},
            "tiv" => %{standard: "Tiv"}, "aln" => %{standard: "Gheg Albanian"},
            "sh" => %{standard: "Serbo-Croatian"}, "fil" => %{...}, ...}
        """
        @spec known_languages() ::
                %{String.t() => %{required(styles()) => String.t()}} | {:error, term()}
        @spec known_languages(Locale.locale_name() | LanguageTag.t()) ::
                %{String.t() => %{required(styles()) => String.t()}} | {:error, term()}
        def known_languages(locale \\ get_locale())

        def known_languages(%LanguageTag{cldr_locale_name: cldr_locale_name}) do
          known_languages(cldr_locale_name)
        end

        # Implement available_locales/known_locales
        for locale_name <- Locale.Loader.known_locale_names(config) do
          languages = locale_name |> Locale.Loader.get_locale(config) |> Map.get(:languages)

          def available_languages(unquote(locale_name)) do
            unquote(Enum.sort(Map.keys(languages)))
          end

          def known_languages(unquote(locale_name)) do
            unquote(Macro.escape(languages))
          end
        end

        def available_languages(locale), do: {:error, Locale.locale_error(locale)}

        def known_languages(locale), do: {:error, Locale.locale_error(locale)}

        @doc """
        Try to translate the given language iso code or language tag.

        ## Example

            iex> #{inspect(__MODULE__)}.Language.to_string("eo")
            {:ok, "Esperanto"}
        """
        @spec to_string(Locale.language() | LanguageTag.t()) ::
                {:ok, String.t()} | {:error, term()}
        @spec to_string(Locale.language() | LanguageTag.t(), Keyword.t()) ::
                {:ok, String.t()} | {:error, term()}
        def to_string(key, options \\ []) do
          opts =
            %{}
            |> merge_style(options[:style])
            |> merge_locale(options[:locale])
            |> merge_fallback(options[:fallback])

          with {:ok, locale} <- backend().validate_locale(opts.locale) do
            result = to_string_by_locale(key, locale, opts)

            if result == :error && Map.fetch!(opts, :fallback) do
              to_string_by_locale(key, default_locale(), opts)
            else
              result
            end
          end
        end

        defp to_string_by_locale(%LanguageTag{language: language}, locale, opts) do
          to_string_by_locale(language, locale, opts)
        end

        defp to_string_by_locale(language, locale, %{style: style}) do
          with {:ok, lang} <- locale.cldr_locale_name |> known_languages() |> Map.fetch(language) do
            case Map.fetch(lang, style) do
              {:ok, _} = val -> val
              :error -> Map.fetch(lang, :standard)
            end
          end
        end

        # Merge style into options
        # Only allow @styles (default to standard)
        defp merge_style(opts, nil), do: Map.put(opts, :style, :standard)
        defp merge_style(opts, style) when style in @styles, do: Map.put(opts, :style, style)

        defp merge_style(opts, style) do
          msg =
            "Invalid :style  option #{inspect(style)} supplied. " <>
              "Valid styles are #{inspect(@styles)}."

          raise ArgumentError, msg
        end

        # Merge locale into options
        # Default to cldr's current locale
        defp merge_locale(opts, nil), do: Map.put(opts, :locale, get_locale())
        defp merge_locale(opts, locale), do: Map.put(opts, :locale, locale)

        # Merge fallback into options
        # Only allow booleans (default to false)
        defp merge_fallback(opts, nil), do: Map.put(opts, :fallback, false)

        defp merge_fallback(opts, fallback) when is_boolean(fallback),
          do: Map.put(opts, :fallback, fallback)

        defp merge_fallback(opts, fallback) do
          msg =
            "Invalid :fallback  option #{inspect(fallback)} supplied. " <>
              "Valid fallbacks are #{inspect([true, false])}."

          raise ArgumentError, msg
        end
      end
    end
  end
end