lib/cldr/gettext/plural.ex

defmodule Cldr.Gettext.Plural do
  @moduledoc """
  Implements a macro to define a CLDR-based Gettext plural
  module

  [gettext](https://hexdocs.pm/gettext) allows for user-defined
  [plural forms](https://hexdocs.pm/gettext/Gettext.Plural.html#content) modules
  to be configured for a [gettext backend](https://hexdocs.pm/gettext/Gettext.Backend.html#content).

  To define a plural forms module that uses [CLDR plural rules](https://cldr.unicode.org/index/cldr-spec/plural-rules)
  create a new module and then `use Cldr.Gettext.Plural`. For example:

      defmodule MyApp.Gettext.Plural do
        use Cldr.Gettext.Plural, cldr_backend: MyApp.Cldr

      end

  This module can then be used in the configuration of a `gettext` backend.
  For example:

      defmodule MyApp.Gettext do
        use Gettext, plural_forms: MyApp.Gettext.Plural

      end

  """

  @doc """
  Configure a module to be a [gettext plural](https://hexdocs.pm/gettext/Gettext.Plural.html#content)
  module.

  A CLDR-based `gettext` plural module is defined by including `use Cldr.Gettext.Plural`
  as in this example:

      defmodule MyApp.Gettext.Plural do
        use Cldr.Gettext.Plural, cldr_backend: MyApp.Cldr

      end

  ## Arguments

  * `options` is a keyword list of options. The default is `[]`

  ## Options

  * `:cldr_backend` is any CLDR [backend](https://hexdocs.pm/ex_cldr/readme.html#backend-module-configuration)
    module. The default is `Cldr.default_backend!/0`.

  """
  defmacro __using__(opts \\ []) do
    backend = Keyword.get_lazy(opts, :cldr_backend, &Cldr.default_backend!/0)

    quote location: :keep do
      @behaviour Elixir.Gettext.Plural

      alias Cldr.LanguageTag
      alias Cldr.Locale

      @rules Cldr.Config.cldr_data_dir()
             |> Path.join("/plural_rules.json")
             |> File.read!()
             |> Cldr.Config.json_library().decode!
             |> Map.get("cardinal")
             |> Cldr.Config.normalize_plural_rules()
             |> Map.new()


      @nplurals_range [0, 1, 2, 3, 4, 5]
      @gettext_nplurals @rules
                        |> Enum.map(fn {locale, rules} ->
                          {locale, Keyword.keys(rules) |> Enum.zip(@nplurals_range)}
                        end)
                        |> Map.new()

      @doc """
      Returns the number of plural forms for a given locale.

      * `locale` is either a locale name in the list
        `#{unquote(inspect(backend))}.known_locale_names/0` or
        a `%LanguageTag{}` as returned by `Cldr.Locale.new/2`

      ## Examples

          iex> #{inspect(__MODULE__)}.nplurals("pl")
          4

          iex> #{inspect(__MODULE__)}.nplurals("en")
          2

      """
      @spec nplurals(Locale.locale_name() | String.t()) :: pos_integer() | no_return()

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

      def nplurals(locale_name) when is_atom(locale_name) do
        gettext_nplurals()
        |> Map.fetch!(locale_name)
        |> Enum.count()
      end

      def nplurals(locale_name) when is_binary(locale_name) do
        locale_name = String.to_existing_atom(locale_name)
        nplurals(locale_name)
      rescue ArgumentError ->
        raise KeyError, "Key #{inspect locale_name} not found"
      end

      @doc """
      Returns the plural form of a number for a given
      locale.

      * `locale` is either a locale name in the list `#{unquote(inspect(backend))}.known_locale_names/0` or
        a `%LanguageTag{}` as returned by `Cldr.Locale.new/2`

      ## Examples

          iex> #{inspect(__MODULE__)}.plural("pl", 1)
          0

          iex> #{inspect(__MODULE__)}.plural("pl", 2)
          1

          iex> #{inspect(__MODULE__)}.plural("pl", 5)
          2

          iex> #{inspect(__MODULE__)}.plural("pl", 112)
          2

          iex> #{inspect(__MODULE__)}.plural("en", 1)
          0

          iex> #{inspect(__MODULE__)}.plural("en", 2)
          1

          iex> #{inspect(__MODULE__)}.plural("en", 112)
          1

      """
      @spec plural(String.t() | LanguageTag.t(), number()) ::
              0 | pos_integer() | no_return()

      def plural(%LanguageTag{cldr_locale_name: cldr_locale_name} = locale, n) do
        rule = unquote(backend).Number.Cardinal.plural_rule(n, cldr_locale_name)

        gettext_nplurals()
        |> Map.get(cldr_locale_name)
        |> Keyword.get(rule)
      end

      def plural(locale_name, n) do
        with {:ok, locale} <- unquote(backend).validate_locale(locale_name) do
          plural(locale, n)
        else
          {:error, _reason} -> raise Elixir.Gettext.Plural.UnknownLocaleError, locale_name
        end
      end

      defp gettext_nplurals do
        @gettext_nplurals
      end
    end
  end
end