Repository: elixir-gettext/gettext
Pull request #393 (2024-08-19 13:00:28)

Refactor Gettext.Macros to generate less code
Talked about this with @josevalim this morning.

coveralls: ## Pull Request Test Coverage Report for [Build 0419b2e9d8220a2c56b0e25641886c5cdf8b55bc-PR-393](https://coveralls.io/builds/69296613)


### Details

* **80** of **85**   **(94.12%)**  changed or added relevant lines in **2** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**2.4%**) to **93.373%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [lib/gettext/macros.ex](https://coveralls.io/builds/69296613/source?filename=lib%2Fgettext%2Fmacros.ex#L110) | 76 | 81 | 93.83%
<!-- | **Total:** | **80** | **85** | **94.12%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [lib/gettext/macros.ex](https://coveralls.io/builds/69296613/source?filename=lib%2Fgettext%2Fmacros.ex#L109) | 1 | 94.0% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/69296613/badge)](https://coveralls.io/builds/69296613) |
| :-- | --: |
| Change from base [Build 0cdb06adc52161dae532e9bd99e80a83e5472c64](https://coveralls.io/builds/69291482): |  2.4% |
| Covered Lines: | 634 |
| Relevant Lines: | 679 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/lib/gettext.ex b/lib/gettext.ex
index 0b0821b..2ca747f 100644
--- a/lib/gettext.ex
+++ b/lib/gettext.ex
@@ -598,6 +598,16 @@ defmodule Gettext do
   @type backend :: module
   @type bindings :: map() | Keyword.t()
 
+  @typedoc """
+  A Gettext domain.
+
+  See [*Domains*](#module-domains) in the module documentation for more information.
+  """
+  @typedoc since: "0.26.0"
+  @type domain() :: :default | binary()
+
+  defguardp is_domain(domain) when domain == :default or is_binary(domain)
+
   @doc false
   defmacro __using__(opts) do
     opts =
@@ -775,7 +785,7 @@ defmodule Gettext do
 
   """
   @doc section: :translation
-  @spec dpgettext(module, binary, binary | nil, binary, bindings) :: binary
+  @spec dpgettext(module, domain, binary | nil, binary, bindings) :: binary
   def dpgettext(backend, domain, msgctxt, msgid, bindings \\ %{})
 
   def dpgettext(backend, domain, msgctxt, msgid, bindings) when is_list(bindings) do
@@ -783,7 +793,8 @@ defmodule Gettext do
   end
 
   def dpgettext(backend, domain, msgctxt, msgid, bindings)
-      when is_atom(backend) and is_binary(domain) and is_binary(msgid) and is_map(bindings) do
+      when is_atom(backend) and is_domain(domain) and is_binary(msgid) and is_map(bindings) do
+    domain = domain_or_default(backend, domain)
     locale = get_locale(backend)
     result = backend.lgettext(locale, domain, msgctxt, msgid, bindings)
     handle_backend_result(result, backend, locale, domain, msgctxt, msgid)
@@ -820,7 +831,7 @@ defmodule Gettext do
 
   """
   @doc section: :translation
-  @spec dgettext(module, binary, binary, bindings) :: binary
+  @spec dgettext(module, domain, binary, bindings) :: binary
   def dgettext(backend, domain, msgid, bindings \\ %{}) do
     dpgettext(backend, domain, nil, msgid, bindings)
   end
@@ -900,7 +911,7 @@ defmodule Gettext do
 
   """
   @doc section: :translation
-  @spec dpngettext(module, binary, binary | nil, binary, binary, non_neg_integer, bindings) ::
+  @spec dpngettext(module, domain, binary | nil, binary, binary, non_neg_integer, bindings) ::
           binary
   def dpngettext(backend, domain, msgctxt, msgid, msgid_plural, n, bindings \\ %{})
 
@@ -910,8 +921,9 @@ defmodule Gettext do
   end
 
   def dpngettext(backend, domain, msgctxt, msgid, msgid_plural, n, bindings)
-      when is_atom(backend) and is_binary(domain) and is_binary(msgid) and is_binary(msgid_plural) and
+      when is_atom(backend) and is_domain(domain) and is_binary(msgid) and is_binary(msgid_plural) and
              is_integer(n) and n >= 0 and is_map(bindings) do
+    domain = domain_or_default(backend, domain)
     locale = get_locale(backend)
     result = backend.lngettext(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
     handle_backend_result(result, backend, locale, domain, msgctxt, msgid)
@@ -943,7 +955,7 @@ defmodule Gettext do
 
   """
   @doc section: :translation
-  @spec dngettext(module, binary, binary, binary, non_neg_integer, bindings) :: binary
+  @spec dngettext(module, domain, binary, binary, non_neg_integer, bindings) :: binary
   def dngettext(backend, domain, msgid, msgid_plural, n, bindings \\ %{}),
     do: dpngettext(backend, domain, nil, msgid, msgid_plural, n, bindings)
 
@@ -1125,4 +1137,7 @@ defmodule Gettext do
 
     backend.handle_missing_bindings(exception, incomplete)
   end
+
+  defp domain_or_default(backend, :default), do: backend.__gettext__(:default_domain)
+  defp domain_or_default(_backend, domain) when is_binary(domain), do: domain
 end
diff --git a/lib/gettext/compiler.ex b/lib/gettext/compiler.ex
index 1b618ea..c005573 100644
--- a/lib/gettext/compiler.ex
+++ b/lib/gettext/compiler.ex
@@ -97,74 +97,48 @@ defmodule Gettext.Compiler do
     end)
   end
 
+  # TODO: remove once we deprecate "use Gettext" for generating backends.
   def generate_macros(_env) do
     quote unquote: false do
-      defmacro dpgettext_noop(domain, msgctxt, msgid) do
-        domain = Gettext.Compiler.expand_to_binary(domain, "domain", __CALLER__)
-        msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __CALLER__)
-        msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __CALLER__)
+      require Gettext.Macros
 
-        if Gettext.Extractor.extracting?() do
-          Gettext.Extractor.extract(
-            __CALLER__,
-            __MODULE__,
-            domain,
-            msgctxt,
-            msgid,
-            Gettext.Compiler.get_and_flush_extracted_comments()
-          )
-        end
-
-        msgid
+      defmacro dpgettext_noop(domain, msgctxt, msgid) do
+        Gettext.Macros.dpgettext_noop_with_backend(__MODULE__, domain, msgctxt, msgid)
       end
 
       defmacro dgettext_noop(domain, msgid) do
         quote do
-          unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid))
+          Gettext.Macros.dgettext_noop_with_backend(__MODULE__, unquote(domain), unquote(msgid))
         end
       end
 
       defmacro gettext_noop(msgid) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid))
+          Gettext.Macros.gettext_noop_with_backend(__MODULE__, unquote(msgid))
         end
       end
 
       defmacro pgettext_noop(msgid, context) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpgettext_noop(unquote(domain), unquote(context), unquote(msgid))
+          Gettext.Macros.pgettext_noop_with_backend(__MODULE__, context, unquote(msgid))
         end
       end
 
       defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do
-        domain = Gettext.Compiler.expand_to_binary(domain, "domain", __CALLER__)
-        msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __CALLER__)
-        msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __CALLER__)
-        msgid_plural = Gettext.Compiler.expand_to_binary(msgid_plural, "msgid_plural", __CALLER__)
-
-        if Gettext.Extractor.extracting?() do
-          Gettext.Extractor.extract(
-            __CALLER__,
-            __MODULE__,
-            domain,
-            msgctxt,
-            {msgid, msgid_plural},
-            Gettext.Compiler.get_and_flush_extracted_comments()
-          )
-        end
-
-        {msgid, msgid_plural}
+        Gettext.Macros.dpgettext_noop_with_backend(
+          __MODULE__,
+          domain,
+          msgctxt,
+          msgid,
+          msgid_plural
+        )
       end
 
       defmacro dngettext_noop(domain, msgid, msgid_plural) do
         quote do
-          unquote(__MODULE__).dpngettext_noop(
+          Gettext.Macros.dngettext_noop_with_backend(
+            __MODULE__,
             unquote(domain),
-            nil,
             unquote(msgid),
             unquote(msgid_plural)
           )
@@ -172,12 +146,10 @@ defmodule Gettext.Compiler do
       end
 
       defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpngettext_noop(
-            unquote(domain),
-            unquote(msgctxt),
+          Gettext.Macros.pngettext_noop_with_backend(
+            __MODULE__,
+            msgctxt,
             unquote(msgid),
             unquote(msgid_plural)
           )
@@ -185,12 +157,9 @@ defmodule Gettext.Compiler do
       end
 
       defmacro ngettext_noop(msgid, msgid_plural) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpngettext_noop(
-            unquote(domain),
-            nil,
+          Gettext.Macros.ngettext_noop_with_backend(
+            __MODULE__,
             unquote(msgid),
             unquote(msgid_plural)
           )
@@ -199,14 +168,11 @@ defmodule Gettext.Compiler do
 
       defmacro dpgettext(domain, msgctxt, msgid, bindings \\ Macro.escape(%{})) do
         quote do
-          msgid =
-            unquote(__MODULE__).dpgettext_noop(unquote(domain), unquote(msgctxt), unquote(msgid))
-
-          Gettext.dpgettext(
-            unquote(__MODULE__),
+          Gettext.Macros.dpgettext_with_backend(
+            __MODULE__,
             unquote(domain),
             unquote(msgctxt),
-            msgid,
+            unquote(msgid),
             unquote(bindings)
           )
         end
@@ -214,16 +180,19 @@ defmodule Gettext.Compiler do
 
       defmacro dgettext(domain, msgid, bindings \\ Macro.escape(%{})) do
         quote do
-          unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings))
+          Gettext.Macros.dgettext_with_backend(
+            __MODULE__,
+            unquote(domain),
+            unquote(msgid),
+            unquote(bindings)
+          )
         end
       end
 
       defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpgettext(
-            unquote(domain),
+          Gettext.Macros.pgettext_with_backend(
+            __MODULE__,
             unquote(msgctxt),
             unquote(msgid),
             unquote(bindings)
@@ -232,29 +201,19 @@ defmodule Gettext.Compiler do
       end
 
       defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings))
+          Gettext.Macros.gettext_with_backend(__MODULE__, unquote(msgid), unquote(bindings))
         end
       end
 
       defmacro dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
         quote do
-          {msgid, msgid_plural} =
-            unquote(__MODULE__).dpngettext_noop(
-              unquote(domain),
-              unquote(msgctxt),
-              unquote(msgid),
-              unquote(msgid_plural)
-            )
-
-          Gettext.dpngettext(
-            unquote(__MODULE__),
+          Gettext.Macros.dpngettext_with_backend(
+            __MODULE__,
             unquote(domain),
             unquote(msgctxt),
-            msgid,
-            msgid_plural,
+            unquote(msgid),
+            unquote(msgid_plural),
             unquote(n),
             unquote(bindings)
           )
@@ -263,9 +222,9 @@ defmodule Gettext.Compiler do
 
       defmacro dngettext(domain, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
         quote do
-          unquote(__MODULE__).dpngettext(
+          Gettext.Macros.dngettext_with_backend(
+            __MODULE__,
             unquote(domain),
-            nil,
             unquote(msgid),
             unquote(msgid_plural),
             unquote(n),
@@ -275,12 +234,9 @@ defmodule Gettext.Compiler do
       end
 
       defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpngettext(
-            unquote(domain),
-            nil,
+          Gettext.Macros.ngettext_with_backend(
+            __MODULE__,
             unquote(msgid),
             unquote(msgid_plural),
             unquote(n),
@@ -290,11 +246,9 @@ defmodule Gettext.Compiler do
       end
 
       defmacro pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-        domain = __gettext__(:default_domain)
-
         quote do
-          unquote(__MODULE__).dpngettext(
-            unquote(domain),
+          Gettext.Macros.pngettext_with_backend(
+            __MODULE__,
             unquote(msgctxt),
             unquote(msgid),
             unquote(msgid_plural),
@@ -305,104 +259,9 @@ defmodule Gettext.Compiler do
       end
 
       defmacro gettext_comment(comment) do
-        comment = Gettext.Compiler.expand_to_binary(comment, "comment", __CALLER__)
-        Gettext.Compiler.append_extracted_comment(comment)
-        :ok
-      end
-    end
-  end
-
-  @doc """
-  Expands the given `msgid` in the given `env`, raising if it doesn't expand to
-  a binary.
-  """
-  @spec expand_to_binary(binary, binary, Macro.Env.t()) :: binary | no_return
-  def expand_to_binary(term, what, env)
-      when what in ~w(domain msgctxt msgid msgid_plural comment) do
-    gettext_module =
-      if Module.open?(env.module) do
-        Module.get_attribute(env.module, :__gettext_backend__)
-      else
-        List.first(env.module.__info__(:attributes)[:__gettext_backend__])
+        Gettext.Macros.gettext_comment(comment)
       end
-
-    raiser = fn term ->
-      raise ArgumentError, """
-      Gettext macros expect message keys (msgid and msgid_plural),
-      domains, and comments to expand to strings at compile-time, but the given #{what}
-      doesn't. This is what the macro received:
-
-      #{inspect(term)}
-
-      Dynamic messages should be avoided as they limit Gettext's
-      ability to extract messages from your source code. If you are
-      sure you need dynamic lookup, you can use the functions in the Gettext
-      module:
-
-          string = "hello world"
-          Gettext.gettext(#{if(gettext_module, do: inspect(gettext_module), else: "backend")}, string)
-      """
     end
-
-    # We support nil too in order to fall back to a nil context and always use the *p
-    # variants of the Gettext macros.
-    case Macro.expand(term, env) do
-      term when is_binary(term) or is_nil(term) ->
-        term
-
-      {:<<>>, _, pieces} = term ->
-        if Enum.all?(pieces, &is_binary/1), do: Enum.join(pieces), else: raiser.(term)
-
-      other ->
-        raiser.(other)
-    end
-  end
-
-  @doc """
-  Expands the given `term` to a compile-time atom in the given `env`.
-  """
-  @spec expand_backend(Macro.t(), Macro.Env.t()) :: module
-  def expand_backend(term, env) do
-    case Macro.expand(term, env) do
-      term when is_atom(term) and term not in [nil, false, true] ->
-        term
-
-      _other ->
-        raise ArgumentError, """
-        Gettext.Macros macros (that end with "_with_backend") expect the backend argument
-        to be an atom at compile-time, but the given term doesn't. This is what the macro
-        received:
-
-        #{inspect(term)}
-
-        Dynamic messages should be avoided as they limit Gettext's
-        ability to extract messages from your source code. If you are
-        sure you need dynamic lookup, you can use the functions in the Gettext
-        module:
-
-            string = "hello world"
-            Gettext.gettext(backend, string)
-        """
-    end
-  end
-
-  @doc """
-  Appends the given comment to the list of extracted comments in the process dictionary.
-  """
-  @spec append_extracted_comment(binary) :: :ok
-  def append_extracted_comment(comment) do
-    existing = Process.get(:gettext_comments, [])
-    Process.put(:gettext_comments, [" " <> comment | existing])
-    :ok
-  end
-
-  @doc """
-  Returns all extracted comments in the process dictionary and clears them from the process
-  dictionary.
-  """
-  @spec get_and_flush_extracted_comments() :: [binary]
-  def get_and_flush_extracted_comments() do
-    Enum.reverse(Process.delete(:gettext_comments) || [])
   end
 
   @doc """
diff --git a/lib/gettext/macros.ex b/lib/gettext/macros.ex
index f87fe94..062284d 100644
--- a/lib/gettext/macros.ex
+++ b/lib/gettext/macros.ex
@@ -7,21 +7,34 @@ defmodule Gettext.Macros do
   Macros enable users to use gettext and get **automatic extraction** of translations.
   See `Gettext` for more information.
 
-  This module is *imported* every time you call:
+  The macros in this module *that don't end with `_with_backend`* are imported
+  every time you call:
 
       use Gettext, backend: MyApp.Gettext
 
-  > #### Warning {: .error}
-  >
-  > You are not meant to use this module in any way other than with `use Gettext` as shown above,
-  > as its macros depend on internals that get set up when you call `use Gettext`.
+  ### Explicit backend
+
+  If you need to use the macros here with an explicit backend and you want extraction
+  to work, you can use the `_with_backend` versions of the macros in this module explicitly
+  instead.
+
+      defmodule MyApp.Gettext do
+        use Gettext, otp_app: :my_app
+      end
+
+      defmodule MyApp.Controller do
+        require Gettext.Macros
+
+        def index(conn, _params) do
+          Gettext.Macros.gettext_with_backend(MyApp.Gettext, "Hello, world!")
+        end
+      end
+
   """
 
   @moduledoc since: "0.26.0"
 
-  @doc false
-  def __expand_runtime_domain__(backend, :default), do: backend.__gettext__(:default_domain)
-  def __expand_runtime_domain__(_backend, domain) when is_binary(domain), do: domain
+  alias Gettext.Extractor
 
   @doc """
   Marks the given message for extraction and returns it unchanged.
@@ -37,24 +50,7 @@ defmodule Gettext.Macros do
 
   """
   defmacro dpgettext_noop(domain, msgctxt, msgid) do
-    domain = expand_domain(domain, __CALLER__)
-    msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __CALLER__)
-    msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __CALLER__)
-
-    if Gettext.Extractor.extracting?() do
-      backend = Module.get_attribute(__CALLER__.module, :__gettext_backend__)
-
-      Gettext.Extractor.extract(
-        __CALLER__,
-        backend,
-        domain,
-        msgctxt,
-        msgid,
-        Gettext.Compiler.get_and_flush_extracted_comments()
-      )
-    end
-
-    msgid
+    extract_singular_translation(__CALLER__, backend(__CALLER__), domain, msgctxt, msgid)
   end
 
   @doc """
@@ -71,9 +67,7 @@ defmodule Gettext.Macros do
 
   """
   defmacro dgettext_noop(domain, msgid) do
-    quote do
-      unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid))
-    end
+    extract_singular_translation(__CALLER__, backend(__CALLER__), domain, _msgctxt = nil, msgid)
   end
 
   @doc """
@@ -90,9 +84,13 @@ defmodule Gettext.Macros do
 
   """
   defmacro gettext_noop(msgid) do
-    quote do
-      unquote(__MODULE__).dpgettext_noop(:default, nil, unquote(msgid))
-    end
+    extract_singular_translation(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      _msgctxt = nil,
+      msgid
+    )
   end
 
   @doc """
@@ -109,9 +107,7 @@ defmodule Gettext.Macros do
 
   """
   defmacro pgettext_noop(msgid, context) do
-    quote do
-      unquote(__MODULE__).dpgettext_noop(:default, unquote(context), unquote(msgid))
-    end
+    extract_singular_translation(__CALLER__, backend(__CALLER__), :default, context, msgid)
   end
 
   @doc """
@@ -128,25 +124,14 @@ defmodule Gettext.Macros do
 
   """
   defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do
-    domain = expand_domain(domain, __CALLER__)
-    msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __CALLER__)
-    msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __CALLER__)
-    msgid_plural = Gettext.Compiler.expand_to_binary(msgid_plural, "msgid_plural", __CALLER__)
-
-    if Gettext.Extractor.extracting?() do
-      backend = Module.get_attribute(__CALLER__.module, :__gettext_backend__)
-
-      Gettext.Extractor.extract(
-        __CALLER__,
-        backend,
-        domain,
-        msgctxt,
-        {msgid, msgid_plural},
-        Gettext.Compiler.get_and_flush_extracted_comments()
-      )
-    end
-
-    {msgid, msgid_plural}
+    extract_plural_translation(
+      __CALLER__,
+      backend(__CALLER__),
+      domain,
+      msgctxt,
+      msgid,
+      msgid_plural
+    )
   end
 
   @doc """
@@ -167,14 +152,14 @@ defmodule Gettext.Macros do
 
   """
   defmacro dngettext_noop(domain, msgid, msgid_plural) do
-    quote do
-      unquote(__MODULE__).dpngettext_noop(
-        unquote(domain),
-        nil,
-        unquote(msgid),
-        unquote(msgid_plural)
-      )
-    end
+    extract_plural_translation(
+      __CALLER__,
+      backend(__CALLER__),
+      domain,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural
+    )
   end
 
   @doc """
@@ -191,14 +176,14 @@ defmodule Gettext.Macros do
 
   """
   defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do
-    quote do
-      unquote(__MODULE__).dpngettext_noop(
-        :default,
-        unquote(msgctxt),
-        unquote(msgid),
-        unquote(msgid_plural)
-      )
-    end
+    extract_plural_translation(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      msgctxt,
+      msgid,
+      msgid_plural
+    )
   end
 
   @doc """
@@ -206,9 +191,14 @@ defmodule Gettext.Macros do
   per-backend configured default domain if provided.
   """
   defmacro ngettext_noop(msgid, msgid_plural) do
-    quote do
-      unquote(__MODULE__).dpngettext_noop(:default, nil, unquote(msgid), unquote(msgid_plural))
-    end
+    extract_plural_translation(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural
+    )
   end
 
   @doc """
@@ -219,20 +209,14 @@ defmodule Gettext.Macros do
   See also `Gettext.dpgettext/5`.
   """
   defmacro dpgettext(domain, msgctxt, msgid, bindings \\ Macro.escape(%{})) do
-    domain = expand_domain(domain, __CALLER__)
-
-    quote do
-      msgid =
-        unquote(__MODULE__).dpgettext_noop(unquote(domain), unquote(msgctxt), unquote(msgid))
-
-      Gettext.dpgettext(
-        @__gettext_backend__,
-        unquote(__MODULE__).__expand_runtime_domain__(@__gettext_backend__, unquote(domain)),
-        unquote(msgctxt),
-        msgid,
-        unquote(bindings)
-      )
-    end
+    singular_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      domain,
+      msgctxt,
+      msgid,
+      bindings
+    )
   end
 
   @doc """
@@ -243,9 +227,14 @@ defmodule Gettext.Macros do
   See also `Gettext.dgettext/4`.
   """
   defmacro dgettext(domain, msgid, bindings \\ Macro.escape(%{})) do
-    quote do
-      unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings))
-    end
+    singular_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      domain,
+      _msgctxt = nil,
+      msgid,
+      bindings
+    )
   end
 
   @doc """
@@ -256,14 +245,14 @@ defmodule Gettext.Macros do
   See also `Gettext.pgettext/4`.
   """
   defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do
-    quote do
-      unquote(__MODULE__).dpgettext(
-        :default,
-        unquote(msgctxt),
-        unquote(msgid),
-        unquote(bindings)
-      )
-    end
+    singular_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      msgctxt,
+      msgid,
+      bindings
+    )
   end
 
   @doc """
@@ -273,9 +262,14 @@ defmodule Gettext.Macros do
   See also `Gettext.gettext/3`.
   """
   defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do
-    quote do
-      unquote(__MODULE__).dpgettext(:default, nil, unquote(msgid), unquote(bindings))
-    end
+    singular_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      _msgctxt = nil,
+      msgid,
+      bindings
+    )
   end
 
   @doc """
@@ -288,27 +282,16 @@ defmodule Gettext.Macros do
   See also `Gettext.dpngettext/7`.
   """
   defmacro dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-    domain = expand_domain(domain, __CALLER__)
-
-    quote do
-      {msgid, msgid_plural} =
-        unquote(__MODULE__).dpngettext_noop(
-          unquote(domain),
-          unquote(msgctxt),
-          unquote(msgid),
-          unquote(msgid_plural)
-        )
-
-      Gettext.dpngettext(
-        @__gettext_backend__,
-        unquote(__MODULE__).__expand_runtime_domain__(@__gettext_backend__, unquote(domain)),
-        unquote(msgctxt),
-        msgid,
-        msgid_plural,
-        unquote(n),
-        unquote(bindings)
-      )
-    end
+    plural_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      domain,
+      msgctxt,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
   end
 
   @doc """
@@ -321,16 +304,16 @@ defmodule Gettext.Macros do
   See also `Gettext.dngettext/6`.
   """
   defmacro dngettext(domain, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-    quote do
-      unquote(__MODULE__).dpngettext(
-        unquote(domain),
-        nil,
-        unquote(msgid),
-        unquote(msgid_plural),
-        unquote(n),
-        unquote(bindings)
-      )
-    end
+    plural_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      domain,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
   end
 
   @doc """
@@ -340,16 +323,16 @@ defmodule Gettext.Macros do
   See also `Gettext.ngettext/5`.
   """
   defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-    quote do
-      unquote(__MODULE__).dpngettext(
-        :default,
-        nil,
-        unquote(msgid),
-        unquote(msgid_plural),
-        unquote(n),
-        unquote(bindings)
-      )
-    end
+    plural_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
   end
 
   @doc """
@@ -361,16 +344,16 @@ defmodule Gettext.Macros do
   See also `Gettext.pngettext/6`.
   """
   defmacro pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-    quote do
-      unquote(__MODULE__).dpngettext(
-        :default,
-        unquote(msgctxt),
-        unquote(msgid),
-        unquote(msgid_plural),
-        unquote(n),
-        unquote(bindings)
-      )
-    end
+    plural_extract_and_translate(
+      __CALLER__,
+      backend(__CALLER__),
+      _domain = :default,
+      msgctxt,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
   end
 
   @doc """
@@ -396,234 +379,127 @@ defmodule Gettext.Macros do
 
   """
   defmacro gettext_comment(comment) do
-    comment = Gettext.Compiler.expand_to_binary(comment, "comment", __CALLER__)
-    Gettext.Compiler.append_extracted_comment(comment)
+    comment = expand_to_binary(comment, "comment", __CALLER__)
+    append_extracted_comment(comment)
     :ok
   end
 
-  ## Macros that also take a backend.
+  ## Singular no-op macros (with backend).
 
   @doc """
   Same as `dpgettext_noop/3`, but takes an explicit backend.
   """
   defmacro dpgettext_noop_with_backend(backend, domain, msgctxt, msgid) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-    domain = expand_domain(domain, __CALLER__)
-    msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __CALLER__)
-    msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __CALLER__)
-
-    if Gettext.Extractor.extracting?() do
-      Gettext.Extractor.extract(
-        __CALLER__,
-        backend,
-        domain,
-        msgctxt,
-        msgid,
-        Gettext.Compiler.get_and_flush_extracted_comments()
-      )
-    end
-
-    msgid
+    extract_singular_translation(__CALLER__, backend, domain, msgctxt, msgid)
   end
 
   @doc """
   Same as `dgettext_noop/2`, but takes an explicit backend.
   """
   defmacro dgettext_noop_with_backend(backend, domain, msgid) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpgettext_noop_with_backend(
-        unquote(backend),
-        unquote(domain),
-        nil,
-        unquote(msgid)
-      )
-    end
+    extract_singular_translation(__CALLER__, backend, domain, _msgctxt = nil, msgid)
   end
 
   @doc """
-  Same as `gettext_noop/1`, but takes an explicit backend.
+  Same as `pgettext_noop/2`, but takes an explicit backend.
   """
-  defmacro gettext_noop_with_backend(backend, msgid) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpgettext_noop_with_backend(
-        unquote(backend),
-        :default,
-        nil,
-        unquote(msgid)
-      )
-    end
+  defmacro pgettext_noop_with_backend(backend, msgctxt, msgid) do
+    extract_singular_translation(__CALLER__, backend, _domain = :default, msgctxt, msgid)
   end
 
   @doc """
-  Same as `pgettext_noop/2`, but takes an explicit backend.
+  Same as `gettext_noop/1`, but takes an explicit backend.
   """
-  defmacro pgettext_noop_with_backend(backend, msgid, context) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpgettext_noop_with_backend(
-        unquote(backend),
-        :default,
-        unquote(context),
-        unquote(msgid)
-      )
-    end
+  defmacro gettext_noop_with_backend(backend, msgid) do
+    extract_singular_translation(__CALLER__, backend, _domain = :default, _msgctxt = nil, msgid)
   end
 
+  ## Plural no-op macros (with backend).
+
   @doc """
   Same as `dpngettext_noop/4`, but takes an explicit backend.
   """
   defmacro dpngettext_noop_with_backend(backend, domain, msgctxt, msgid, msgid_plural) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-    domain = expand_domain(domain, __CALLER__)
-    msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __CALLER__)
-    msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __CALLER__)
-    msgid_plural = Gettext.Compiler.expand_to_binary(msgid_plural, "msgid_plural", __CALLER__)
-
-    if Gettext.Extractor.extracting?() do
-      Gettext.Extractor.extract(
-        __CALLER__,
-        backend,
-        domain,
-        msgctxt,
-        {msgid, msgid_plural},
-        Gettext.Compiler.get_and_flush_extracted_comments()
-      )
-    end
-
-    {msgid, msgid_plural}
+    extract_plural_translation(__CALLER__, backend, domain, msgctxt, msgid, msgid_plural)
   end
 
   @doc """
   Same as `dngettext_noop/3`, but takes an explicit backend.
   """
   defmacro dngettext_noop_with_backend(backend, domain, msgid, msgid_plural) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpngettext_noop_with_backend(
-        unquote(backend),
-        unquote(domain),
-        nil,
-        unquote(msgid),
-        unquote(msgid_plural)
-      )
-    end
+    extract_plural_translation(__CALLER__, backend, domain, _msgctxt = nil, msgid, msgid_plural)
   end
 
   @doc """
   Same as `pngettext_noop/3`, but takes an explicit backend.
   """
   defmacro pngettext_noop_with_backend(backend, msgctxt, msgid, msgid_plural) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpngettext_noop_with_backend(
-        unquote(backend),
-        :default,
-        unquote(msgctxt),
-        unquote(msgid),
-        unquote(msgid_plural)
-      )
-    end
+    extract_plural_translation(
+      __CALLER__,
+      backend,
+      _domain = :default,
+      msgctxt,
+      msgid,
+      msgid_plural
+    )
   end
 
   @doc """
   Same as `ngettext_noop/2`, but takes an explicit backend.
   """
   defmacro ngettext_noop_with_backend(backend, msgid, msgid_plural) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpngettext_noop_with_backend(
-        unquote(backend),
-        :default,
-        nil,
-        unquote(msgid),
-        unquote(msgid_plural)
-      )
-    end
+    extract_plural_translation(
+      __CALLER__,
+      backend,
+      _domain = :default,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural
+    )
   end
 
+  ## Singular macros (with backend).
+
   @doc """
   Same as `dpgettext/4`, but takes an explicit backend.
   """
   defmacro dpgettext_with_backend(backend, domain, msgctxt, msgid, bindings \\ Macro.escape(%{})) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-    domain = expand_domain(domain, __CALLER__)
-
-    quote do
-      msgid =
-        unquote(__MODULE__).dpgettext_noop_with_backend(
-          unquote(backend),
-          unquote(domain),
-          unquote(msgctxt),
-          unquote(msgid)
-        )
-
-      Gettext.dpgettext(
-        unquote(backend),
-        unquote(__MODULE__).__expand_runtime_domain__(unquote(backend), unquote(domain)),
-        unquote(msgctxt),
-        msgid,
-        unquote(bindings)
-      )
-    end
+    singular_extract_and_translate(__CALLER__, backend, domain, msgctxt, msgid, bindings)
   end
 
   @doc """
   Same as `dgettext/3`, but takes an explicit backend.
   """
   defmacro dgettext_with_backend(backend, domain, msgid, bindings \\ Macro.escape(%{})) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpgettext_with_backend(
-        unquote(backend),
-        unquote(domain),
-        nil,
-        unquote(msgid),
-        unquote(bindings)
-      )
-    end
+    singular_extract_and_translate(__CALLER__, backend, domain, _msgctxt = nil, msgid, bindings)
   end
 
   @doc """
   Same as `pgettext/3`, but takes an explicit backend.
   """
   defmacro pgettext_with_backend(backend, msgctxt, msgid, bindings \\ Macro.escape(%{})) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpgettext_with_backend(
-        unquote(backend),
-        :default,
-        unquote(msgctxt),
-        unquote(msgid),
-        unquote(bindings)
-      )
-    end
+    singular_extract_and_translate(
+      __CALLER__,
+      backend,
+      _domain = :default,
+      msgctxt,
+      msgid,
+      bindings
+    )
   end
 
   @doc """
   Same as `gettext/2`, but takes an explicit backend.
   """
   defmacro gettext_with_backend(backend, msgid, bindings \\ Macro.escape(%{})) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-
-    quote do
-      unquote(__MODULE__).dpgettext_with_backend(
-        unquote(backend),
-        :default,
-        nil,
-        unquote(msgid),
-        unquote(bindings)
-      )
-    end
+    singular_extract_and_translate(
+      __CALLER__,
+      backend,
+      _domain = :default,
+      _msgctxt = nil,
+      msgid,
+      bindings
+    )
   end
 
   @doc """
@@ -638,29 +514,16 @@ defmodule Gettext.Macros do
              n,
              bindings \\ Macro.escape(%{})
            ) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
-    domain = expand_domain(domain, __CALLER__)
-
-    quote do
-      {msgid, msgid_plural} =
-        unquote(__MODULE__).dpngettext_noop_with_backend(
-          unquote(backend),
-          unquote(domain),
-          unquote(msgctxt),
-          unquote(msgid),
-          unquote(msgid_plural)
-        )
-
-      Gettext.dpngettext(
-        unquote(backend),
-        unquote(__MODULE__).__expand_runtime_domain__(unquote(backend), unquote(domain)),
-        unquote(msgctxt),
-        msgid,
-        msgid_plural,
-        unquote(n),
-        unquote(bindings)
-      )
-    end
+    plural_extract_and_translate(
+      __CALLER__,
+      backend,
+      domain,
+      msgctxt,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
   end
 
   @doc """
@@ -674,57 +537,134 @@ defmodule Gettext.Macros do
              n,
              bindings \\ Macro.escape(%{})
            ) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
+    plural_extract_and_translate(
+      __CALLER__,
+      backend,
+      domain,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
+  end
 
-    quote do
-      unquote(__MODULE__).dpngettext_with_backend(
-        unquote(backend),
-        unquote(domain),
-        nil,
-        unquote(msgid),
-        unquote(msgid_plural),
-        unquote(n),
-        unquote(bindings)
-      )
-    end
+  @doc """
+  Same as `pngettext/5`, but takes an explicit backend.
+  """
+  defmacro pngettext_with_backend(
+             backend,
+             msgctxt,
+             msgid,
+             msgid_plural,
+             n,
+             bindings \\ Macro.escape(%{})
+           ) do
+    plural_extract_and_translate(
+      __CALLER__,
+      backend,
+      _domain = :default,
+      msgctxt,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
   end
 
   @doc """
   Same as `ngettext/4`, but takes an explicit backend.
   """
   defmacro ngettext_with_backend(backend, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
+    plural_extract_and_translate(
+      __CALLER__,
+      backend,
+      _domain = :default,
+      _msgctxt = nil,
+      msgid,
+      msgid_plural,
+      n,
+      bindings
+    )
+  end
+
+  ## Helpers
+
+  defp extract_singular_translation(env, backend, domain, msgctxt, msgid) do
+    backend = expand_backend(backend, env)
+    domain = expand_domain(domain, env)
+    msgid = expand_to_binary(msgid, "msgid", env)
+    msgctxt = expand_to_binary(msgctxt, "msgctxt", env)
+
+    if Extractor.extracting?() do
+      Extractor.extract(
+        env,
+        backend,
+        domain,
+        msgctxt,
+        msgid,
+        get_and_flush_extracted_comments()
+      )
+    end
+
+    msgid
+  end
+
+  defp extract_plural_translation(env, backend, domain, msgctxt, msgid, msgid_plural) do
+    backend = expand_backend(backend, env)
+    domain = expand_domain(domain, env)
+    msgid = expand_to_binary(msgid, "msgid", env)
+    msgctxt = expand_to_binary(msgctxt, "msgctxt", env)
+    msgid_plural = expand_to_binary(msgid_plural, "msgid_plural", env)
+
+    if Extractor.extracting?() do
+      Extractor.extract(
+        env,
+        backend,
+        domain,
+        msgctxt,
+        {msgid, msgid_plural},
+        get_and_flush_extracted_comments()
+      )
+    end
+
+    {msgid, msgid_plural}
+  end
+
+  defp singular_extract_and_translate(env, backend, domain, msgctxt, msgid, bindings) do
+    domain = expand_domain(domain, env)
+    msgid = extract_singular_translation(env, backend, domain, msgctxt, msgid)
 
     quote do
-      unquote(__MODULE__).dpngettext_with_backend(
+      Gettext.dpgettext(
         unquote(backend),
-        :default,
-        nil,
+        unquote(domain),
+        unquote(msgctxt),
         unquote(msgid),
-        unquote(msgid_plural),
-        unquote(n),
         unquote(bindings)
       )
     end
   end
 
-  @doc """
-  Same as `pngettext/5`, but takes an explicit backend.
-  """
-  defmacro pngettext_with_backend(
-             backend,
-             msgctxt,
-             msgid,
-             msgid_plural,
-             n,
-             bindings \\ Macro.escape(%{})
-           ) do
-    backend = Gettext.Compiler.expand_backend(backend, __CALLER__)
+  defp plural_extract_and_translate(
+         env,
+         backend,
+         domain,
+         msgctxt,
+         msgid,
+         msgid_plural,
+         n,
+         bindings
+       ) do
+    domain = expand_domain(domain, env)
+
+    {msgid, msgid_plural} =
+      extract_plural_translation(env, backend, domain, msgctxt, msgid, msgid_plural)
 
     quote do
-      unquote(__MODULE__).dpngettext_with_backend(
+      Gettext.dpngettext(
         unquote(backend),
-        :default,
+        unquote(domain),
         unquote(msgctxt),
         unquote(msgid),
         unquote(msgid_plural),
@@ -734,13 +674,91 @@ defmodule Gettext.Macros do
     end
   end
 
-  ## Helpers
+  defp expand_domain(:default, _env), do: :default
+  defp expand_domain(domain, env), do: expand_to_binary(domain, "domain", env)
+
+  defp backend(%Macro.Env{} = env) do
+    if Module.open?(env.module) do
+      Module.get_attribute(env.module, :__gettext_backend__) ||
+        raise "expected to find non-nil @__gettext_backend__ attribute in #{inspect(env.module)}"
+    else
+      List.first(env.module.__info__(:attributes)[:__gettext_backend__]) ||
+        raise "expected to find non-nil @__gettext_backend__ attribute in #{inspect(env.module)}"
+    end
+  end
+
+  defp expand_to_binary(term, what, %Macro.Env{} = env)
+       when what in ~w(domain msgctxt msgid msgid_plural comment) do
+    gettext_module =
+      if Module.open?(env.module) do
+        Module.get_attribute(env.module, :__gettext_backend__)
+      else
+        List.first(env.module.__info__(:attributes)[:__gettext_backend__])
+      end
 
-  defp expand_domain(:default, _env) do
-    :default
+    raiser = fn term ->
+      raise ArgumentError, """
+      Gettext macros expect message keys (msgid and msgid_plural),
+      domains, and comments to expand to strings at compile-time, but the given #{what}
+      doesn't. This is what the macro received:
+
+      #{inspect(term)}
+
+      Dynamic messages should be avoided as they limit Gettext's
+      ability to extract messages from your source code. If you are
+      sure you need dynamic lookup, you can use the functions in the Gettext
+      module:
+
+          string = "hello world"
+          Gettext.gettext(#{if(gettext_module, do: inspect(gettext_module), else: "backend")}, string)
+      """
+    end
+
+    # We support nil too in order to fall back to a nil context and always use the *p
+    # variants of the Gettext macros.
+    case Macro.expand(term, env) do
+      term when is_binary(term) or is_nil(term) ->
+        term
+
+      {:<<>>, _, pieces} = term ->
+        if Enum.all?(pieces, &is_binary/1), do: Enum.join(pieces), else: raiser.(term)
+
+      other ->
+        raiser.(other)
+    end
+  end
+
+  defp expand_backend(term, %Macro.Env{} = env) do
+    case Macro.expand(term, env) do
+      term when is_atom(term) and term not in [nil, false, true] ->
+        term
+
+      _other ->
+        raise ArgumentError, """
+        Gettext.Macros macros (that end with "_with_backend") expect the backend argument
+        to be an atom at compile-time, but the given term doesn't. This is what the macro
+        received:
+
+        #{inspect(term)}
+
+        Dynamic messages should be avoided as they limit Gettext's
+        ability to extract messages from your source code. If you are
+        sure you need dynamic lookup, you can use the functions in the Gettext
+        module:
+
+            string = "hello world"
+            Gettext.gettext(backend, string)
+        """
+    end
+  end
+
+  defp append_extracted_comment(comment) do
+    existing = Process.get(:gettext_comments, [])
+    Process.put(:gettext_comments, [" " <> comment | existing])
+    :ok
   end
 
-  defp expand_domain(domain, env) do
-    Gettext.Compiler.expand_to_binary(domain, "domain", env)
+  defp get_and_flush_extracted_comments() do
+    Enum.reverse(Process.delete(:gettext_comments) || [])
   end
 end
diff --git a/mix.exs b/mix.exs
index 260aeea..ac21ea6 100644
--- a/mix.exs
+++ b/mix.exs
@@ -33,8 +33,11 @@ defmodule Gettext.Mixfile do
           "Locale Functions": &(&1[:section] == :locale),
 
           # Gettext.Macros
+          "Macros with Backend":
+            &(&1[:module] == Gettext.Macros and to_string(&1[:name]) =~ ~r/_with_backend$/),
           "Comment Macros": &(&1[:module] == Gettext.Macros and &1[:name] == :gettext_comment),
-          "No-op Macros": &(&1[:module] == Gettext.Macros and to_string(&1[:name]) =~ ~r/_noop$/),
+          "Extraction Macros":
+            &(&1[:module] == Gettext.Macros and to_string(&1[:name]) =~ ~r/_noop$/),
           "Translation Macros": &(&1[:module] == Gettext.Macros)
         ]
       ]


Test patch:
diff --git a/test/gettext/macros_test.exs b/test/gettext/macros_test.exs
index bcce13a..d54547a 100644
--- a/test/gettext/macros_test.exs
+++ b/test/gettext/macros_test.exs
@@ -10,6 +10,8 @@ defmodule Gettext.MacrosTest do
 
   import ExUnit.CaptureLog
 
+  require Gettext.Macros, as: Macros
+
   @backend Gettext.MacrosTest.Translator
   @gettext_msgid "Hello world"
 
@@ -255,4 +257,139 @@ defmodule Gettext.MacrosTest do
     assert pngettext_noop("test", "One message", "%{count} messages") ==
              {"One message", "%{count} messages"}
   end
+
+  ## _with_backend variants
+
+  describe "dpgettext_noop_with_backend/4" do
+    test "supports test with context based messages" do
+      assert Macros.dpgettext_noop_with_backend(@backend, "test", "ctx", "Hello world") ==
+               "Hello world"
+    end
+  end
+
+  describe "dgettext_noop_with_backend/4" do
+    test "supports test with context based messages" do
+      assert Macros.dgettext_noop_with_backend(@backend, "test", "Hello world") == "Hello world"
+    end
+  end
+
+  describe "pgettext_noop_with_backend/4" do
+    test "supports test with context based messages" do
+      assert Macros.pgettext_noop_with_backend(@backend, "ctx", "Hello world") == "Hello world"
+    end
+  end
+
+  describe "gettext_noop_with_backend/2" do
+    test "supports test with context based messages" do
+      assert Macros.gettext_noop_with_backend(@backend, "Hello world") == "Hello world"
+    end
+  end
+
+  describe "dpngettext_noop_with_backend/5" do
+    test "supports test with context based messages" do
+      assert Macros.dpngettext_noop_with_backend(
+               @backend,
+               "test",
+               "ctx",
+               "One message",
+               "%{count} messages"
+             ) ==
+               {"One message", "%{count} messages"}
+    end
+  end
+
+  describe "dngettext_noop_with_backend/4" do
+    test "supports test with context based messages" do
+      assert Macros.dngettext_noop_with_backend(
+               @backend,
+               "test",
+               "One message",
+               "%{count} messages"
+             ) ==
+               {"One message", "%{count} messages"}
+    end
+  end
+
+  describe "pngettext_noop_with_backend/4" do
+    test "supports test with context based messages" do
+      assert Macros.pngettext_noop_with_backend(
+               @backend,
+               "ctx",
+               "One message",
+               "%{count} messages"
+             ) ==
+               {"One message", "%{count} messages"}
+    end
+  end
+
+  describe "ngettext_noop_with_backend/3" do
+    test "supports test with context based messages" do
+      assert Macros.ngettext_noop_with_backend(@backend, "One message", "%{count} messages") ==
+               {"One message", "%{count} messages"}
+    end
+  end
+
+  describe "translation macros *_with_backend" do
+    setup do
+      Gettext.put_locale(@backend, "it")
+      :ok
+    end
+
+    test "dpgettext_with_backend/5" do
+      assert Macros.dpgettext_with_backend(@backend, "default", "test", "Hello world", %{}) ==
+               "Ciao mondo"
+    end
+
+    test "dgettext_with_backend/4" do
+      assert Macros.dgettext_with_backend(@backend, "default", "Hello world") == "Ciao mondo"
+    end
+
+    test "pgettext_with_backend/4" do
+      assert Macros.pgettext_with_backend(@backend, "test", "Hello world") == "Ciao mondo"
+    end
+
+    test "gettext_with_backend/2" do
+      assert Macros.gettext_with_backend(@backend, "Hello world") == "Ciao mondo"
+    end
+
+    test "dpngettext_with_backend/6" do
+      assert Macros.dpngettext_with_backend(
+               @backend,
+               "default",
+               "test",
+               "One new email",
+               "%{count} new emails",
+               1
+             ) == "Una nuova test email"
+    end
+
+    test "dngettext_with_backend/5" do
+      assert Macros.dngettext_with_backend(
+               @backend,
+               "default",
+               "One new email",
+               "%{count} new emails",
+               1
+             ) == "Una nuova email"
+    end
+
+    test "pngettext_with_backend/5" do
+      assert Macros.pngettext_with_backend(
+               @backend,
+               "test",
+               "One new email",
+               "%{count} new emails",
+               1
+             ) == "Una nuova test email"
+    end
+
+    test "ngettext_with_backend/4" do
+      assert Macros.ngettext_with_backend(
+               @backend,
+               "One new email",
+               "%{count} new emails",
+               1
+             ) == "Una nuova email"
+    end
+  end
 end