Repository: elixir-gettext/gettext (elixir)

Issue:
Configurable default domain?
I’ve been working with a couple of different JS implementations of gettext (before rolling a fork of one of them to work better with Vue), and it appears that _most_ JS implementations use `messages` as their default domain—and have an easy way to change the active default domain.

As best as I can tell, this would be set by calling `textdomain(domainname)` in the C version of gettext. What I think would be useful would be providing a `domain` value to `use Gettext`:

```elixir
defmodule MyApp.Gettext do
  use Gettext, otp_app: :my_app, domain: "messages"
end
```

I can work up a PR for this if that makes sense.

Pull request description:
Configurable Default Domain
Resolves #253

A given backend can be configured to have a default domain with the option
`:default_domain`, as in:

```elixir
config :gettext, MyApp.Gettext, default_domain: "messages"

defmodule MyApp.Gettext do
  use Gettext, otp_app: :my_app, default_domain: "messages"
end
```

When applied, this means that instead of `MyApp.Gettext.gettext("foo")` being
equivalent to `MyApp.Gettext.dgettext("default", "foo")`, it is now equivalent
to `MyApp.Gettext.gettext("messages", "foo")`.

Patch:
diff --git a/lib/gettext.ex b/lib/gettext.ex
index 7fe1a17..2a68276 100644
--- a/lib/gettext.ex
+++ b/lib/gettext.ex
@@ -42,8 +42,9 @@ defmodule Gettext do
         3
       )
 
-  The arguments for the Gettext macros and their order can be derived from
-  their names. For `dpgettext/4` the arguments are: `domain`, `context`, `msgid`, `bindings` (default to `%{}`).
+  The arguments for the Gettext macros and their order can be derived froe
+  their names. For `dpgettext/4` the arguments are: `domain`, `context`,
+  `msgid`, `bindings` (default to `%{}`).
 
   Translations are looked up from `.po` files. In the following sections we will
   explore exactly what are those files before we explore the "Gettext API" in
@@ -152,6 +153,18 @@ defmodule Gettext do
 
       config :my_app, MyApp.Gettext, default_locale: "fr"
 
+  ### Default Domain
+
+  Each backend can be configured with a specific `:default_domain`
+  that replaces `"default"` in `gettext/2`, `pgettext/3`, and `ngettext/4`
+  for that backend.
+
+      defmodule MyApp.Gettext do
+        use Gettext, otp_app: :my_app, default_domain: "messages"
+      end
+
+      config :my_app, MyApp.Gettext, default_domain: "translations"
+
   ## Gettext API
 
   There are two ways to use Gettext:
@@ -274,7 +287,10 @@ defmodule Gettext do
       MyApp.Gettext.dgettext("errors", "Error!")
       #=> "Errore!"
 
-  When `gettext` or `ngettext` are used, the `"default"` domain is used.
+  When backend `gettext`, `ngettext`, or `pgettext` are used, the backend's
+  default domain is used (which defaults to "default"). The `Gettext`
+  functions accepting a backend (`gettext/3`, `ngettext/5`, and `pgettext/4`)
+  _always_ use a domain of "default".
 
   ## Contexts
 
diff --git a/lib/gettext/backend.ex b/lib/gettext/backend.ex
index 26999ed..653882e 100644
--- a/lib/gettext/backend.ex
+++ b/lib/gettext/backend.ex
@@ -140,7 +140,8 @@ defmodule Gettext.Backend do
   @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t()) :: Macro.t()
 
   @doc """
-  Same as `dgettext("default", msgid, %{})`.
+  Same as `dgettext("default", msgid, %{})`, but will use a per-backend
+  configured default domain if provided.
 
   See also `Gettext.gettext/3`.
   """
@@ -242,7 +243,8 @@ defmodule Gettext.Backend do
                  ) :: Macro.t()
 
   @doc """
-  Same as `dngettext("default", msgid, msgid_plural, n, bindings)`.
+  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`.
   """
@@ -305,7 +307,8 @@ defmodule Gettext.Backend do
                  ) :: Macro.t()
 
   @doc """
-  Same as `dngettext_noop("default", msgid, mgsid_plural)`.
+  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()
 
diff --git a/lib/gettext/compiler.ex b/lib/gettext/compiler.ex
index c9bece5..9fbd287 100644
--- a/lib/gettext/compiler.ex
+++ b/lib/gettext/compiler.ex
@@ -11,6 +11,7 @@ defmodule Gettext.Compiler do
   require Logger
 
   @default_priv "priv/gettext"
+  @default_domain "default"
   @po_wildcard "*/LC_MESSAGES/*.po"
 
   @doc false
@@ -39,6 +40,10 @@ defmodule Gettext.Compiler do
     default_locale =
       opts[:default_locale] || quote(do: Application.fetch_env!(:gettext, :default_locale))
 
+    default_domain = opts[:default_domain] || @default_domain
+
+    Module.put_attribute(env.module, :gettext_default_domain, default_domain)
+
     quote do
       @behaviour Gettext.Backend
 
@@ -48,6 +53,7 @@ defmodule Gettext.Compiler do
       def __gettext__(:otp_app), do: unquote(otp_app)
       def __gettext__(:known_locales), do: unquote(known_locales)
       def __gettext__(:default_locale), do: unquote(default_locale)
+      def __gettext__(:default_domain), do: unquote(default_domain)
 
       # The manifest lives in the root of the priv
       # directory that contains .po/.pot files.
@@ -102,14 +108,18 @@ defmodule Gettext.Compiler do
       end
 
       defmacro gettext_noop(msgid) do
+        domain = __gettext__(:default_domain)
+
         quote do
-          unquote(__MODULE__).dpgettext_noop("default", nil, unquote(msgid))
+          unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid))
         end
       end
 
       defmacro pgettext_noop(msgid, context) do
+        domain = __gettext__(:default_domain)
+
         quote do
-          unquote(__MODULE__).dpgettext_noop("default", unquote(context), unquote(msgid))
+          unquote(__MODULE__).dpgettext_noop(unquote(domain), unquote(context), unquote(msgid))
         end
       end
 
@@ -147,9 +157,11 @@ defmodule Gettext.Compiler do
       end
 
       defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do
+        domain = __gettext__(:default_domain)
+
         quote do
           unquote(__MODULE__).dpngettext_noop(
-            "default",
+            unquote(domain),
             unquote(msgctxt),
             unquote(msgid),
             unquote(msgid_plural)
@@ -158,9 +170,11 @@ defmodule Gettext.Compiler do
       end
 
       defmacro ngettext_noop(msgid, msgid_plural) do
+        domain = __gettext__(:default_domain)
+
         quote do
           unquote(__MODULE__).dpngettext_noop(
-            "default",
+            unquote(domain),
             nil,
             unquote(msgid),
             unquote(msgid_plural)
@@ -190,9 +204,11 @@ defmodule Gettext.Compiler do
       end
 
       defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do
+        domain = __gettext__(:default_domain)
+
         quote do
           unquote(__MODULE__).dpgettext(
-            "default",
+            unquote(domain),
             unquote(msgctxt),
             unquote(msgid),
             unquote(bindings)
@@ -201,8 +217,10 @@ defmodule Gettext.Compiler do
       end
 
       defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do
+        domain = __gettext__(:default_domain)
+
         quote do
-          unquote(__MODULE__).dpgettext("default", nil, unquote(msgid), unquote(bindings))
+          unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings))
         end
       end
 
@@ -242,9 +260,11 @@ defmodule Gettext.Compiler do
       end
 
       defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
+        domain = __gettext__(:default_domain)
+
         quote do
           unquote(__MODULE__).dpngettext(
-            "default",
+            unquote(domain),
             nil,
             unquote(msgid),
             unquote(msgid_plural),
@@ -255,9 +275,11 @@ 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(
-            "default",
+            unquote(domain),
             unquote(msgctxt),
             unquote(msgid),
             unquote(msgid_plural),


Test patch:
diff --git a/test/gettext_test.exs b/test/gettext_test.exs
index 630e3f9..dfd37ae 100644
--- a/test/gettext_test.exs
+++ b/test/gettext_test.exs
@@ -23,6 +23,10 @@ defmodule GettextTest.TranslatorWithCustomPluralForms do
   use Gettext, otp_app: :test_application, plural_forms: Plural
 end
 
+defmodule GettextTest.TranslatorWithDefaultDomain do
+  use Gettext, otp_app: :test_application, default_domain: "errors"
+end
+
 defmodule GettextTest.HandleMissingTranslation do
   use Gettext, otp_app: :test_application
 
@@ -46,11 +50,13 @@ defmodule GettextTest do
   alias GettextTest.TranslatorWithCustomPriv
   alias GettextTest.TranslatorWithAllowedLocales
   alias GettextTest.TranslatorWithCustomPluralForms
+  alias GettextTest.TranslatorWithDefaultDomain
   alias GettextTest.HandleMissingTranslation
 
   require Translator
   require TranslatorWithCustomPriv
   require TranslatorWithAllowedLocales
+  require TranslatorWithDefaultDomain
 
   test "the default locale is \"en\"" do
     assert Gettext.get_locale() == "en"
@@ -107,6 +113,11 @@ defmodule GettextTest do
     assert TranslatorWithCustomPriv.__gettext__(:otp_app) == :test_application
   end
 
+  test "__gettext__(:default_domain): returns the default domain for the given backend" do
+    assert Translator.__gettext__(:default_domain) == "default"
+    assert TranslatorWithDefaultDomain.__gettext__(:default_domain) == "errors"
+  end
+
   test "found translations return {:ok, translation}" do
     assert Translator.lgettext("it", "default", nil, "Hello world", %{}) == {:ok, "Ciao mondo"}
 
@@ -141,6 +152,13 @@ defmodule GettextTest do
              {:ok, "Indirizzo email non valido"}
   end
 
+  test "a custom default_domain can be set for a backend" do
+    alias TranslatorWithDefaultDomain, as: T
+    Gettext.put_locale("it")
+    assert T.gettext("Invalid email address") == "Indirizzo email non valido"
+    assert T.gettext("Hello world") == "Hello world"
+  end
+
   test "allowed_locales ignores other locales" do
     assert TranslatorWithAllowedLocales.lgettext("it", "default", nil, "Hello world", %{}) ==
              {:default, "Hello world"}