lib/gettext/backend.ex

defmodule Gettext.Backend do
  @moduledoc """
  Defines a Gettext backend.

  ## Usage

  A Gettext **backend** must `use` this module.

      defmodule MyApp.Gettext do
        use Gettext.Backend, otp_app: :my_app
      end

  Using this module generates all the callbacks required by the `Gettext.Backend`
  behaviour into the module that uses it. For more options and information,
  see `Gettext`.

  > #### `use Gettext.Backend` Is a Recent Feature {: .info}
  >
  > Before version v0.26.0, you could only `use Gettext` to generate a backend.
  >
  > Version v0.26.0 changes the way backends work so that now a Gettext backend
  > must `use Gettext.Backend`, while to use the functions in the backend you
  > will do `use Gettext, backend: MyApp.Gettext`.
  """

  defmacro __using__(opts) do
    # TODO: From Elixir v1.13 onwards, use compile_env and remove this if.
    env_fun = if function_exported?(Module, :attributes_in, 1), do: :compile_env, else: :get_env

    quote do
      require Logger

      opts = unquote(opts)
      otp_app = Keyword.fetch!(opts, :otp_app)

      @gettext_opts opts
                    |> Keyword.merge(Application.unquote(env_fun)(otp_app, __MODULE__, []))
                    |> Keyword.put_new(:interpolation, Gettext.Interpolation.Default)

      @interpolation Keyword.fetch!(@gettext_opts, :interpolation)

      @before_compile Gettext.Compiler

      def handle_missing_bindings(exception, incomplete) do
        _ = Logger.error(Exception.message(exception))
        incomplete
      end

      defoverridable handle_missing_bindings: 2

      def handle_missing_translation(_locale, domain, _msgctxt, msgid, bindings) do
        Gettext.Compiler.warn_if_domain_contains_slashes(domain)

        with {:ok, interpolated} <- @interpolation.runtime_interpolate(msgid, bindings),
             do: {:default, interpolated}
      end

      def handle_missing_plural_translation(
            _locale,
            domain,
            _msgctxt,
            msgid,
            msgid_plural,
            n,
            bindings
          ) do
        Gettext.Compiler.warn_if_domain_contains_slashes(domain)
        string = if n == 1, do: msgid, else: msgid_plural
        bindings = Map.put(bindings, :count, n)

        with {:ok, interpolated} <- @interpolation.runtime_interpolate(string, bindings),
             do: {:default, interpolated}
      end

      defoverridable handle_missing_translation: 5, handle_missing_plural_translation: 7
    end
  end

  @doc """
  Default handling for missing bindings.

  This function is called when there are missing bindings in a message. It
  takes a `Gettext.MissingBindingsError` struct and the message with the
  wrong bindings left as is with the `%{}` syntax.

  For example, if something like this is called:

      gettext("Hello %{name}, your favorite color is %{color}", name: "Jane", color: "blue")

  and our `it/LC_MESSAGES/default.po` looks like this:

      msgid "Hello %{name}, your favorite color is %{color}"
      msgstr "Ciao %{name}, il tuo colore preferito è %{colour}" # (typo)

  then Gettext will call:

      MyApp.Gettext.handle_missing_bindings(exception, "Ciao Jane, il tuo colore preferito è %{colour}")

  where `exception` is a struct that looks like this:

      %Gettext.MissingBindingsError{
        backend: MyApp.Gettext,
        domain: "default",
        locale: "it",
        msgid: "Ciao %{name}, il tuo colore preferito è %{colour}",
        bindings: [:colour],
      }

  The return value of the `c:handle_missing_bindings/2` callback is used as the
  translated string that the message macros and functions return.

  The default implementation for this function uses `Logger.error/1` to warn
  about the missing binding and returns the translated message with the
  incomplete bindings.

  This function can be overridden. For example, to raise when there are missing
  bindings:

      def handle_missing_bindings(exception, _incomplete) do
        raise exception
      end

  """
  @callback handle_missing_bindings(Gettext.MissingBindingsError.t(), binary) ::
              binary | no_return

  @doc """
  Default handling for messages with a missing message.

  When a Gettext function/macro is called with a string to translate
  into a locale but that locale doesn't provide a message for that
  string, this callback is invoked. `msgid` is the string that Gettext
  tried to translate.

  This function should return `{:ok, translated}` if a message can be
  fetched or constructed for the given string. If you cannot find a
  message, it should return `{:default, translated}`, where the
  translated string defaults to the interpolated msgid. You can, however,
  customize the default to, for example, pick the message from the
  default locale. The important is to return `:default` instead of `:ok`
  whenever the result does not quite match the requested locale.

  Earlier versions of this library provided a callback without msgctxt.
  Users implementing that callback will still get the same results,
  but they are encouraged to switch to the new 5-argument version.
  """
  @callback handle_missing_translation(
              Gettext.locale(),
              domain :: String.t(),
              msgctxt :: String.t(),
              msgid :: String.t(),
              bindings :: map()
            ) ::
              {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]}

  @doc """
  Default handling for plural messages with a missing message.

  Same as `c:handle_missing_translation/5`, but for plural messages.
  In this case, `n` is the number used for pluralizing the translated string.

  Earlier versions of this library provided a callback without msgctxt.
  Users implementing that callback will still get the same results,
  but they are encouraged to switch to the new 7-argument version.
  """
  @callback handle_missing_plural_translation(
              Gettext.locale(),
              domain :: String.t(),
              msgctxt :: String.t(),
              msgid :: String.t(),
              msgid_plural :: String.t(),
              n :: non_neg_integer(),
              bindings :: map()
            ) ::
              {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]}

  @doc """
  Translates a message.

  See `Gettext.gettext/3` for more information.
  """
  @doc since: "0.26.0"
  @callback lgettext(
              Gettext.locale(),
              domain :: String.t(),
              msgctxt :: String.t() | nil,
              msgid :: String.t(),
              bindings :: map()
            ) ::
              {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]}

  @doc """
  Translates a plural message.

  See `Gettext.ngettext/5` for more information.
  """
  @doc since: "0.26.0"
  @callback lngettext(
              Gettext.locale(),
              domain :: String.t(),
              msgctxt :: String.t() | nil,
              msgid :: String.t(),
              msgid_plural :: String.t(),
              n :: non_neg_integer(),
              bindings :: map()
            ) ::
              {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]}
end