Repository: elixir-gettext/gettext
Pull request #391 (2024-08-17 14:00:45)

Avoid compile-time dependency to Gettext backend
First pass, let's see if this is a direction we like.

coveralls: ## Pull Request Test Coverage Report for [Build 1eff8d67c642e7e3fae839c81727be1d68eea7e7-PR-391](https://coveralls.io/builds/69277846)


### Details

* **21** of **37**   **(56.76%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-2.2%**) to **90.909%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [lib/gettext.ex](https://coveralls.io/builds/69277846/source?filename=lib%2Fgettext.ex#L634) | 5 | 6 | 83.33%
| [lib/gettext/macros.ex](https://coveralls.io/builds/69277846/source?filename=lib%2Fgettext%2Fmacros.ex#L10) | 9 | 24 | 37.5%
<!-- | **Total:** | **21** | **37** | **56.76%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/69277846/badge)](https://coveralls.io/builds/69277846) |
| :-- | --: |
| Change from base [Build c602ea9d1a2ee5869216a653337b3e66f2b70b10](https://coveralls.io/builds/69276610): |  -2.2% |
| Covered Lines: | 550 |
| Relevant Lines: | 605 |

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

whatyouhide: Yes docs coming up, and deprecation as well. I tested on a small sample app (not Phoenix) and it did but I will test once I update the generatorssss

Patch:
diff --git a/lib/gettext.ex b/lib/gettext.ex
index 73eb3a6..e6f021b 100644
--- a/lib/gettext.ex
+++ b/lib/gettext.ex
@@ -627,19 +627,36 @@ defmodule Gettext do
 
   @doc false
   defmacro __using__(opts) do
-    quote do
-      opts = unquote(opts)
-
-      if Keyword.has_key?(opts, :backend) do
-        raise "not implemented yet"
+    opts =
+      if Macro.quoted_literal?(opts) do
+        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
       else
-        # TODO: Deprecate this branch
-        require Gettext.Backend
-        Gettext.Backend.__using__(opts)
+        opts
       end
+
+    case Keyword.keyword?(opts) && Keyword.fetch(opts, :backend) do
+      {:ok, backend} ->
+        quote do
+          @__gettext_backend__ unquote(backend)
+          import Gettext.Macros
+        end
+
+      _other ->
+        quote do
+          # TODO: Deprecate this branch
+          use Gettext.Backend, unquote(opts)
+        end
     end
   end
 
+  defp expand_alias({:__aliases__, _, _} = als, env) do
+    Macro.expand(als, %{env | function: {:__gettext__, 1}})
+  end
+
+  defp expand_alias(other, _env) do
+    other
+  end
+
   @doc """
   Gets the global Gettext locale for the current process.
 
diff --git a/lib/gettext/backend.ex b/lib/gettext/backend.ex
index aa2cb1f..19b27c2 100644
--- a/lib/gettext/backend.ex
+++ b/lib/gettext/backend.ex
@@ -175,255 +175,34 @@ defmodule Gettext.Backend do
               {:ok, String.t()} | {:default, String.t()} | {:missing_bindings, String.t(), [atom]}
 
   @doc """
-  Translates the given `msgid` with a given context (`msgctxt`) in the given `domain`.
+  Translates a message.
 
-  `bindings` is a map of bindings to support interpolation.
-
-  See also `Gettext.dpgettext/5`.
-  """
-  @macrocallback dpgettext(
-                   domain :: Macro.t(),
-                   msgctxt :: String.t(),
-                   msgid :: String.t(),
-                   bindings :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `dpgettext(domain, msgctxt, msgid, %{})`.
-
-  See also `Gettext.dpgettext/5`.
-  """
-  @macrocallback dpgettext(domain :: Macro.t(), msgctxt :: String.t(), msgid :: String.t()) ::
-                   Macro.t()
-
-  @doc """
-  Translates the given `msgid` in the given `domain`.
-
-  `bindings` is a map of bindings to support interpolation.
-
-  See also `Gettext.dgettext/4`.
-  """
-  @macrocallback dgettext(domain :: Macro.t(), msgid :: String.t(), bindings :: Macro.t()) ::
-                   Macro.t()
-
-  @doc """
-  Same as `dgettext(domain, msgid, %{})`.
-
-  See also `Gettext.dgettext/4`.
-  """
-  @macrocallback dgettext(domain :: Macro.t(), msgid :: String.t()) :: Macro.t()
-
-  @doc """
-  Translates the given `msgid` with the given context (`msgctxt`).
-
-  `bindings` is a map of bindings to support interpolation.
-
-  See also `Gettext.pgettext/4`.
-  """
-  @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t(), bindings :: Macro.t()) ::
-                   Macro.t()
-
-  @doc """
-  Same as `pgettext(msgctxt, msgid, %{})`.
-
-  See also `Gettext.pgettext/4`.
-  """
-  @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t()) :: Macro.t()
-
-  @doc """
-  Same as `dgettext("default", msgid, %{})`, but will use a per-backend
-  configured default domain if provided.
-
-  See also `Gettext.gettext/3`.
-  """
-  @macrocallback gettext(msgid :: String.t(), bindings :: Macro.t()) :: Macro.t()
-
-  @doc """
-  Same as `gettext(msgid, %{})`.
-
-  See also `Gettext.gettext/3`.
-  """
-  @macrocallback gettext(msgid :: String.t()) :: Macro.t()
-
-  @doc """
-  Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`)
-  in the given `domain`.
-
-  `n` is an integer used to determine how to pluralize the
-  message. `bindings` is a map of bindings to support interpolation.
-
-  See also `Gettext.dpngettext/7`.
-  """
-  @macrocallback dpngettext(
-                   domain :: Macro.t(),
-                   msgctxt :: String.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t(),
-                   bindings :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `dpngettext(domain, msgctxt, msgid, msgid_plural, n, %{})`.
-
-  See also `Gettext.dpngettext/7`.
-  """
-  @macrocallback dpngettext(
-                   domain :: Macro.t(),
-                   msgctxt :: String.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Translates the given plural message (`msgid` + `msgid_plural`) in the
-  given `domain`.
-
-  `n` is an integer used to determine how to pluralize the
-  message. `bindings` is a map of bindings to support interpolation.
-
-  See also `Gettext.dngettext/6`.
-  """
-  @macrocallback dngettext(
-                   domain :: Macro.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t(),
-                   bindings :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `dngettext(domain, msgid, msgid_plural, n, %{})`.
-
-  See also `Gettext.dngettext/6`.
-  """
-  @macrocallback dngettext(
-                   domain :: Macro.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`).
-
-  `n` is an integer used to determine how to pluralize the
-  message. `bindings` is a map of bindings to support interpolation.
-
-  See also `Gettext.pngettext/6`.
+  See `Gettext.gettext/3` for more information.
   """
-  @macrocallback pngettext(
-                   msgctxt :: String.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t(),
-                   bindings :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `pngettext(msgctxt, msgid, msgid_plural, n, %{})`.
-
-  See also `Gettext.pngettext/6`.
-  """
-  @macrocallback pngettext(
-                   msgctxt :: String.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `dngettext("default", msgid, msgid_plural, n, bindings)`, but will
-  use a per-backend configured default domain if provided.
-
-  See also `Gettext.ngettext/5`.
-  """
-  @macrocallback ngettext(
-                   msgid :: String.t(),
-                   msgid_plural :: String.t(),
-                   n :: Macro.t(),
-                   bindings :: Macro.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `ngettext(msgid, msgid_plural, n, %{})`.
-
-  See also `Gettext.ngettext/5`.
-  """
-  @macrocallback ngettext(msgid :: String.t(), msgid_plural :: String.t(), n :: Macro.t()) ::
-                   Macro.t()
-
-  @doc """
-  Marks the given message for extraction and returns it unchanged.
-
-  This macro can be used to mark a message for extraction when `mix
-  gettext.extract` is run. The return value is the given string, so that this
-  macro can be used seamlessly in place of the string to extract.
-
-  ## Examples
-
-      MyApp.Gettext.dgettext_noop("errors", "Error found!")
-      #=> "Error found!"
-
-  """
-  @macrocallback dgettext_noop(domain :: String.t(), msgid :: String.t()) :: Macro.t()
-
-  @doc """
-  Same as `dgettext_noop("default", msgid)`.
-  """
-  @macrocallback gettext_noop(msgid :: String.t()) :: Macro.t()
-
-  @doc """
-  Marks the given message for extraction and returns
-  `{msgid, msgid_plural}`.
-
-  This macro can be used to mark a message for extraction when `mix
-  gettext.extract` is run. The return value of this macro is `{msgid,
-  msgid_plural}`.
-
-  ## Examples
-
-      my_fun = fn {msgid, msgid_plural} ->
-        # do something with msgid and msgid_plural
-      end
-
-      my_fun.(MyApp.Gettext.dngettext_noop("errors", "One error", "%{count} errors"))
-
-  """
-  @macrocallback dngettext_noop(
-                   domain :: Macro.t(),
-                   msgid :: String.t(),
-                   msgid_plural :: String.t()
-                 ) :: Macro.t()
-
-  @doc """
-  Same as `dngettext_noop("default", msgid, mgsid_plural)`, but will use a
-  per-backend configured default domain if provided.
-  """
-  @macrocallback ngettext_noop(msgid :: String.t(), msgid_plural :: String.t()) :: Macro.t()
+  @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 """
-  Stores an "extracted comment" for the next message.
-
-  This macro can be used to add comments (Gettext refers to such
-  comments as *extracted comments*) to the next message that will
-  be extracted. Extracted comments will be prefixed with `#.` in POT
-  files.
-
-  Calling this function multiple times will accumulate the comments;
-  when another Gettext macro (such as `c:gettext/2`) is called,
-  the comments will be extracted and attached to that message, and
-  they will be flushed so as to start again.
-
-  This macro always returns `:ok`.
-
-  ## Examples
-
-      MyApp.Gettext.gettext_comment("The next message is awesome")
-      MyApp.Gettext.gettext_comment("Another comment for the next message")
-      MyApp.Gettext.gettext("The awesome message")
+  Translates a plural message.
 
+  See `Gettext.ngettext/5` for more information.
   """
-  @macrocallback gettext_comment(comment :: String.t()) :: :ok
+  @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
diff --git a/lib/gettext/compiler.ex b/lib/gettext/compiler.ex
index 5c5d43f..aa58cda 100644
--- a/lib/gettext/compiler.ex
+++ b/lib/gettext/compiler.ex
@@ -63,8 +63,12 @@ defmodule Gettext.Compiler do
 
       unquote(macros())
 
-      # These are the two functions we generated inside the backend.
+      # These are the two functions we generate inside the backend.
+
+      @impl Gettext.Backend
       def lgettext(locale, domain, msgctxt \\ nil, msgid, bindings)
+
+      @impl Gettext.Backend
       def lngettext(locale, domain, msgctxt \\ nil, msgid, msgid_plural, n, bindings)
 
       unquote(compile_po_files(env, known_po_files, opts))
diff --git a/lib/gettext/extractor.ex b/lib/gettext/extractor.ex
index c6a1d4b..c19b6a7 100644
--- a/lib/gettext/extractor.ex
+++ b/lib/gettext/extractor.ex
@@ -68,10 +68,23 @@ defmodule Gettext.Extractor do
 
   Note that this function doesn't perform any operation on the filesystem.
   """
-  @spec extract(Macro.Env.t(), module, binary, binary, binary | {binary, binary}, [binary]) :: :ok
+  @spec extract(
+          Macro.Env.t(),
+          backend :: module,
+          domain :: binary | :default,
+          msgctxt :: binary,
+          id :: binary | {binary, binary},
+          extracted_comments :: [binary]
+        ) :: :ok
   def extract(%Macro.Env{} = caller, backend, domain, msgctxt, id, extracted_comments) do
     format_flag = backend.__gettext__(:interpolation).message_format()
 
+    domain =
+      case domain do
+        :default -> backend.__gettext__(:default_domain)
+        string when is_binary(string) -> string
+      end
+
     message =
       create_message_struct(
         id,
diff --git a/lib/gettext/macros.ex b/lib/gettext/macros.ex
new file mode 100644
index 0000000..12ad45c
--- /dev/null
+++ b/lib/gettext/macros.ex
@@ -0,0 +1,409 @@
+defmodule Gettext.Macros do
+  @moduledoc """
+  Macros used by Gettext to provide the gettext family of functions.
+
+  *Available since v0.26.0.*
+
+  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:
+
+      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`.
+  """
+
+  @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
+
+  @doc """
+  Marks the given message for extraction and returns it unchanged.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value is the given string, so that this
+  macro can be used seamlessly in place of the string to extract.
+
+  ## Examples
+
+      dpgettext_noop("errors", "Home page", "Error found!")
+      #=> "Error found!"
+
+  """
+  defmacro dpgettext_noop(domain, msgctxt, msgid) do
+    domain = expand_domain(domain, __CALLER__)
+    msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__)
+    msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__)
+
+    if Gettext.Extractor.extracting?() do
+      Gettext.Extractor.extract(
+        __CALLER__,
+        __MODULE__,
+        domain,
+        msgctxt,
+        msgid,
+        Gettext.Compiler.get_and_flush_extracted_comments()
+      )
+    end
+
+    msgid
+  end
+
+  @doc """
+  Marks the given message for extraction and returns it unchanged.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value is the given string, so that this
+  macro can be used seamlessly in place of the string to extract.
+
+  ## Examples
+
+      dgettext_noop("errors", "Error found!")
+      #=> "Error found!"
+
+  """
+  defmacro dgettext_noop(domain, msgid) do
+    quote do
+      unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid))
+    end
+  end
+
+  @doc """
+  Marks the given message for extraction and returns it unchanged.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value is the given string, so that this
+  macro can be used seamlessly in place of the string to extract.
+
+  ## Examples
+
+      gettext_noop("Error found!")
+      #=> "Error found!"
+
+  """
+  defmacro gettext_noop(msgid) do
+    quote do
+      unquote(__MODULE__).dpgettext_noop(:default, nil, unquote(msgid))
+    end
+  end
+
+  @doc """
+  Marks the given message for extraction and returns it unchanged.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value is the given string, so that this
+  macro can be used seamlessly in place of the string to extract.
+
+  ## Examples
+
+      pgettext_noop("Error found!", "Home page")
+      #=> "Error found!"
+
+  """
+  defmacro pgettext_noop(msgid, context) do
+    quote do
+      unquote(__MODULE__).dpgettext_noop(:default, unquote(context), unquote(msgid))
+    end
+  end
+
+  @doc """
+  Marks the given message for extraction and returns it unchanged.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value is the given string, so that this
+  macro can be used seamlessly in place of the string to extract.
+
+  ## Examples
+
+      dpngettext_noop("errors", "Home page", "Error found!", "Errors found!")
+      #=> "Error found!"
+
+  """
+  defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do
+    domain = expand_domain(domain, __CALLER__)
+    msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__)
+    msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__)
+
+    msgid_plural =
+      Gettext.Compiler.expand_to_binary(msgid_plural, "msgid_plural", __MODULE__, __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}
+  end
+
+  @doc """
+  Marks the given message for extraction and returns
+  `{msgid, msgid_plural}`.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value of this macro is `{msgid,
+  msgid_plural}`.
+
+  ## Examples
+
+      my_fun = fn {msgid, msgid_plural} ->
+        # do something with msgid and msgid_plural
+      end
+
+      my_fun.(dngettext_noop("errors", "One error", "%{count} errors"))
+
+  """
+  defmacro dngettext_noop(domain, msgid, msgid_plural) do
+    quote do
+      unquote(__MODULE__).dpngettext_noop(
+        unquote(domain),
+        nil,
+        unquote(msgid),
+        unquote(msgid_plural)
+      )
+    end
+  end
+
+  @doc """
+  Marks the given message for extraction and returns it unchanged.
+
+  This macro can be used to mark a message for extraction when `mix
+  gettext.extract` is run. The return value is the given string, so that this
+  macro can be used seamlessly in place of the string to extract.
+
+  ## Examples
+
+      pngettext_noop("Home page", "Error found!", "Errors found!")
+      #=> "Error found!"
+
+  """
+  defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do
+    quote do
+      unquote(__MODULE__).dpngettext_noop(
+        :default,
+        unquote(msgctxt),
+        unquote(msgid),
+        unquote(msgid_plural)
+      )
+    end
+  end
+
+  @doc """
+  Same as `dngettext_noop("default", msgid, mgsid_plural)`, but will use a
+  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
+  end
+
+  @doc """
+  Translates the given `msgid` with a given context (`msgctxt`) in the given `domain`.
+
+  `bindings` is a map of bindings to support interpolation.
+
+  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
+  end
+
+  @doc """
+  Translates the given `msgid` in the given `domain`.
+
+  `bindings` is a map of bindings to support interpolation.
+
+  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
+  end
+
+  @doc """
+  Translates the given `msgid` with the given context (`msgctxt`).
+
+  `bindings` is a map of bindings to support interpolation.
+
+  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
+  end
+
+  @doc """
+  Same as `dgettext("default", msgid, %{})`, but will use a per-backend
+  configured default domain if provided.
+
+  See also `Gettext.gettext/3`.
+  """
+  defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do
+    quote do
+      unquote(__MODULE__).dpgettext(:default, nil, unquote(msgid), unquote(bindings))
+    end
+  end
+
+  @doc """
+  Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`)
+  in the given `domain`.
+
+  `n` is an integer used to determine how to pluralize the
+  message. `bindings` is a map of bindings to support interpolation.
+
+  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
+  end
+
+  @doc """
+  Translates the given plural message (`msgid` + `msgid_plural`) in the
+  given `domain`.
+
+  `n` is an integer used to determine how to pluralize the
+  message. `bindings` is a map of bindings to support interpolation.
+
+  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
+  end
+
+  @doc """
+  Same as `dngettext("default", msgid, msgid_plural, n, bindings)`, but will
+  use a per-backend configured default domain if provided.
+
+  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
+  end
+
+  @doc """
+  Translates the given plural message (`msgid` + `msgid_plural`) with the given context (`msgctxt`).
+
+  `n` is an integer used to determine how to pluralize the
+  message. `bindings` is a map of bindings to support interpolation.
+
+  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
+  end
+
+  @doc """
+  Stores an "extracted comment" for the next message.
+
+  This macro can be used to add comments (Gettext refers to such
+  comments as *extracted comments*) to the next message that will
+  be extracted. Extracted comments will be prefixed with `#.` in POT
+  files.
+
+  Calling this function multiple times will accumulate the comments;
+  when another Gettext macro (such as `gettext/2`) is called,
+  the comments will be extracted and attached to that message, and
+  they will be flushed so as to start again.
+
+  This macro always returns `:ok`.
+
+  ## Examples
+
+      gettext_comment("The next message is awesome")
+      gettext_comment("Another comment for the next message")
+      gettext("The awesome message")
+
+  """
+  defmacro gettext_comment(comment) do
+    comment = Gettext.Compiler.expand_to_binary(comment, "comment", __MODULE__, __CALLER__)
+    Gettext.Compiler.append_extracted_comment(comment)
+    :ok
+  end
+
+  defp expand_domain(:default, _env) do
+    :default
+  end
+
+  defp expand_domain(domain, env) do
+    Gettext.Compiler.expand_to_binary(domain, "domain", __MODULE__, env)
+  end
+end
diff --git a/mix.exs b/mix.exs
index afcc96e..393883f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -11,6 +11,7 @@ defmodule Gettext.Mixfile do
       app: :gettext,
       version: @version,
       elixir: "~> 1.12",
+      elixirc_paths: elixirc_paths(Mix.env()),
       build_embedded: Mix.env() == :prod,
       deps: deps(),
       preferred_cli_env: [coveralls: :test, "coveralls.html": :test, "coveralls.github": :test],
@@ -47,6 +48,9 @@ defmodule Gettext.Mixfile do
     ]
   end
 
+  defp elixirc_paths(:test), do: ["lib", "test/support"]
+  defp elixirc_paths(_other), do: ["lib"]
+
   defp deps do
     [
       {:expo, "~> 0.5.1 or ~> 1.0"},


Test patch:
diff --git a/test/gettext/new_backend_setup_test.exs b/test/gettext/new_backend_setup_test.exs
new file mode 100644
index 0000000..e523b1a
--- /dev/null
+++ b/test/gettext/new_backend_setup_test.exs
@@ -0,0 +1,62 @@
+defmodule Gettext.NewBackendSetupTest do
+  # https://github.com/elixir-gettext/gettext/issues/330
+  use ExUnit.Case, async: true
+
+  import ExUnit.CaptureIO
+  import GettextTest.MixProjectHelpers
+
+  @moduletag :tmp_dir
+
+  defmodule Backend do
+    use Gettext.Backend, otp_app: :test_application
+  end
+
+  describe "use Gettext, backend: ..." do
+    test "imports Gettext.Macros macros but doesn't generate functions" do
+      {{:module, mod, _bytecode, _funs}, _bindings} =
+        Code.eval_quoted(
+          quote do
+            defmodule MyModule do
+              use Gettext,
+                backend: Gettext.NewBackendSetupTest.Backend
+
+              def translate do
+                gettext("Hello world")
+              end
+            end
+          end
+        )
+
+      refute function_exported?(mod, :__gettext__, 1)
+
+      assert mod.translate() == "Hello world"
+    end
+  end
+
+  describe "compile-time dependencies" do
+    @tag :skip
+    test "are not created for modules that use the backend",
+         %{test: test, tmp_dir: tmp_dir} = context do
+      create_test_mix_file(context)
+
+      write_file(context, "lib/my_app.ex", """
+      defmodule MyApp.Gettext do
+        use Gettext.Backend, otp_app: #{inspect(test)}
+      end
+
+      defmodule MyApp do
+        use Gettext, backend: MyApp.Gettext
+      end
+      """)
+
+      output =
+        in_project(test, tmp_dir, fn _module ->
+          capture_io(fn -> Mix.Task.run("compile") end)
+          capture_io(fn -> Mix.Task.run("xref", ["trace", "lib/my_app.ex"]) end)
+        end)
+
+      assert output =~ ~r"lib/my_app\.ex:\d+: alias MyApp\.Gettext \(runtime\)\n"
+      refute output =~ ~r"lib/my_app\.ex:\d+: alias MyApp\.Gettext \(compile\)\n"
+    end
+  end
+end
diff --git a/test/mix/tasks/gettext.extract_test.exs b/test/mix/tasks/gettext.extract_test.exs
index ee35420..c56fe15 100644
--- a/test/mix/tasks/gettext.extract_test.exs
+++ b/test/mix/tasks/gettext.extract_test.exs
@@ -2,6 +2,7 @@ defmodule Mix.Tasks.Gettext.ExtractTest do
   use ExUnit.Case
 
   import ExUnit.CaptureIO
+  import GettextTest.MixProjectHelpers
 
   @moduletag :tmp_dir
 
@@ -11,10 +12,6 @@ defmodule Mix.Tasks.Gettext.ExtractTest do
     :ok
   end
 
-  defp in_project(module, dir, fun) do
-    Mix.Project.in_project(module, dir, [prune_code_paths: false], fun)
-  end
-
   test "extracting and extracting with --merge", %{test: test, tmp_dir: tmp_dir} = context do
     create_test_mix_file(context)
 
@@ -188,32 +185,6 @@ defmodule Mix.Tasks.Gettext.ExtractTest do
     end)
   end
 
-  defp create_test_mix_file(context, gettext_config \\ []) do
-    write_file(context, "mix.exs", """
-    defmodule MyApp.MixProject do
-      use Mix.Project
-
-      def project() do
-        [app: #{inspect(context.test)}, version: "0.1.0", gettext: #{inspect(gettext_config)}]
-      end
-
-      def application() do
-        [extra_applications: [:logger, :gettext]]
-      end
-    end
-    """)
-  end
-
-  defp write_file(context, path, contents) do
-    path = Path.join(context.tmp_dir, path)
-    File.mkdir_p!(Path.dirname(path))
-    File.write!(path, contents)
-  end
-
-  defp read_file(context, path) do
-    context.tmp_dir |> Path.join(path) |> File.read!()
-  end
-
   defp run(args) do
     Mix.Tasks.Gettext.Extract.run(args)
   end
diff --git a/test/support/mix_project_helpers.ex b/test/support/mix_project_helpers.ex
new file mode 100644
index 0000000..4e6c427
--- /dev/null
+++ b/test/support/mix_project_helpers.ex
@@ -0,0 +1,31 @@
+defmodule GettextTest.MixProjectHelpers do
+  def create_test_mix_file(context, gettext_config \\ []) do
+    write_file(context, "mix.exs", """
+    defmodule MyApp.MixProject do
+      use Mix.Project
+
+      def project() do
+        [app: #{inspect(context.test)}, version: "0.1.0", gettext: #{inspect(gettext_config)}]
+      end
+
+      def application() do
+        [extra_applications: [:logger, :gettext]]
+      end
+    end
+    """)
+  end
+
+  def write_file(context, path, contents) do
+    path = Path.join(context.tmp_dir, path)
+    File.mkdir_p!(Path.dirname(path))
+    File.write!(path, contents)
+  end
+
+  def read_file(context, path) do
+    context.tmp_dir |> Path.join(path) |> File.read!()
+  end
+
+  def in_project(module, dir, fun) do
+    Mix.Project.in_project(module, dir, [prune_code_paths: false], fun)
+  end
+end