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 @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() # This is a very unsophisticated fallback mechanism to # resolve the plurals for a given locale. It only falls # back to the language part of a language tag. 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 case Map.fetch(gettext_nplurals(), locale_name) do {:ok, plurals} -> Enum.count(plurals) :error -> case String.split(Atom.to_string(locale_name), "-") do [string_locale_name] -> raise KeyError, "Key #{inspect(locale_name)} not found" [language | _rest] -> nplurals(language) end end end # We are using String.to_atom/1 which is not a preferred # option however it is not being called with user input # and will only be called from Gettext configuration so # the risk is considered mitigated. It is used because # when compiling modules that `use` this one we cannot # guarantee that the configured CLDR locales are compiled # and therefore we cannot assume that the atoms representing # those locales have been instantiated. def nplurals(locale_name) when is_binary(locale_name) do locale_name |> Cldr.Config.locale_name_from_posix() |> String.to_atom() |> nplurals() 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 iex> #{inspect(__MODULE__)}.plural("en_GB", 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) case Map.fetch(gettext_nplurals(), cldr_locale_name) do {:ok, rules} -> Keyword.fetch!(rules, rule) :error -> case String.split(Atom.to_string(cldr_locale_name), "-") do [string_locale_name] -> raise KeyError, "Key #{inspect(cldr_locale_name)} not found" [language | _rest] -> rule = unquote(backend).Number.Cardinal.plural_rule(n, String.to_atom(language)) gettext_nplurals() |> Map.fetch!(String.to_atom(language)) |> Keyword.fetch!(rule) end end 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 @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() defp gettext_nplurals do @gettext_nplurals end end end end