lib/backpex.ex

defmodule Backpex do
  @moduledoc """
  Backpex provides an easy way to manage existing resources in your application.
  """

  @doc false
  defmacro __(msg, live_resource \\ nil) do
    {msg, opts} =
      case msg do
        {msg, opts} -> {msg, opts}
        msg -> {msg, Macro.escape(%{})}
      end

    quote do
      use Gettext, backend: Backpex.Gettext

      # mark translation for extraction
      Gettext.Macros.dgettext_noop("backpex", unquote(msg))

      Backpex.translate({unquote(msg), unquote(opts)}, unquote(live_resource))
    end
  end

  @doc """
  Translates a text with the configured translator_function. If a live_resource is given, it calls the LiveResource's translate callback.

  ## Examples

      Backpex.translate("Hello")

      Backpex.translate({"Hello %{name}", %{name: "World"}})

      Backpex.translate("Welcome", MyApp.LiveResource)
  """
  def translate(msg, live_resource \\ nil)

  def translate(msg, live_resource) when is_binary(msg) do
    translate({msg, %{}}, live_resource)
  end

  def translate({msg, opts}, nil = _live_resource) do
    translate_func = translate_func(:general)
    translate_func.({msg, opts})
  end

  def translate({msg, opts}, live_resource) do
    live_resource.translate({msg, opts})
  end

  @doc """
  Translates an error text with the configured error_translator_function.

  ## Examples

      Backpex.translate_error("can't be blank")

      Backpex.translate_error({"must be greater than %{number}", %{number: 0}})
  """
  def translate_error(msg) when is_binary(msg) do
    translate_error({msg, %{}})
  end

  def translate_error({msg, opts}) do
    translate_func = translate_func(:error)
    translate_func.({msg, opts})
  end

  defp translate_func(type) when type in [:general, :error] do
    key =
      case type do
        :error -> :error_translator_function
        :general -> :translator_function
      end

    case Application.get_env(:backpex, key) do
      {module, function} ->
        &apply(module, function, [&1])

      nil ->
        IO.warn("""
        Backpex can't find the #{inspect(key)} configuration in your config.

        Hint: you can set up a `translator_function` and `error_translator_function` to route all strings to your application helpers:

        config :backpex,
          translator_function: {MyAppWeb.CoreComponents, :translate_backpex},
          error_translator_function: {MyAppWeb.CoreComponents, :translate_error}

        A fallback message translator will be used.
        """)

        &default_translate/1
    end
  end

  defp default_translate({msg, opts}) do
    Enum.reduce(opts, msg, fn {key, value}, acc ->
      try do
        String.replace(acc, "%{#{key}}", to_string(value))
      rescue
        e ->
          IO.warn(
            """
            The fallback message translator of Backpex could not handle replacing the given value.

            Given value: #{inspect(value)}

            Exception: #{Exception.message(e)}
            """,
            __STACKTRACE__
          )

          "invalid value"
      end
    end)
  end
end