Gettext translations are not extracted when using a dynamic backend

Hello everyone!

I am developing a library that contains translations. Users of this library can define a gettext backend in the config.exs script used by my library. Gettext provides functions that take a backend as a parameter. I want to use these functions to translate text in my library, but the translations are not extracted by the mix gettext.extract task.

Suppose I have a module DemoWeb.Gettext with use Gettext in it:

If I have this function in my code, everything works as expected and the translations are extracted when the mix task runs:

DemoWeb.Gettext.dgettext("domain", "translation")

However, when I pass the backend as a parameter, the translation is not extracted:

Gettext.dgettext(DemoWeb.Gettext, "domain", "translation")

Is this because in this function, which accepts a dynamic backend, gettext does not know which backend is used at compile time, because it could be dynamic, and therefore cannot extract the translations?

Is there a better way to integrate gettext into a library or automatically extract translations with a backend defined in the config.exs script?

Thanks in advance. I appreciate any help!

The MyApp.Gettext.*_noop set of macros are designed to help in this situation. They mark the messages for extraction but do not otherwise include any code in your module.

I think the right approach is:

  • Define a Gettext backend in your library in your own namespace
  • Call MyLib.Gettext.dgettext_noop("domain, "message") for each message you want to be extracted.
  • Call the Gettext.dgettext(DemoWeb.Gettext, "domain", "translation") function as you normally would.

This ends up with two calls per message but one is a macro invoked at compile time (to extract the messages only) and the other is a function invoked at runtime to perform the translation.

3 Likes

Hey @kip, may I ask you to elaborate where you’d put each function in your application, please?

I’m currently trying to define messages for multiple domains, but to fetch the translation with a dynamic domain.

I came up with something like this, but I don’t like that I have to repeat the dgettext_noop_with_backend/3 call for every domain plus the default domain:

defmodule DemoWeb.Domains do
  require Gettext.Macros

  defmacro dtext(domain, message) do
    quote do
      Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, "default", unquote(message))
      Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, "domain1", unquote(message))
      Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, "domain2", unquote(message))

      Gettext.dgettext(DemoWeb.Gettext, unquote(domain), unquote(message))
    end
  end
end

# In demo_web.ex
  defp html_helpers do
    quote do
      use Gettext, backend: DemoWeb.Gettext

      import DemoWeb.Domains
    end
  end

# home.html.heex
{dtext(Enum.random(["default", "domain1", "domain2"]), "hello world")}

If I refactor the three calls into a loop, I sadly receive the following error:

defmodule DemoWeb.Domains do
  require Gettext.Macros

  defmacro dtext(domain, message) do
    quote do
      for d <- ["default", "domain1", "domain2"] do
        Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, d, unquote(message))
      end

      Gettext.dgettext(DemoWeb.Gettext, unquote(domain), unquote(message))
    end
  end
end
(ArgumentError) Gettext macros expect message keys (msgid and msgid_plural),
domains, and comments to expand to strings at compile-time, but the given domain
doesn't. This is what the macro received:

{:d, [line: 1, counter: {DemoWeb.PageHTML, 23}], DemoWeb.Domains}

That for loop needs to go outside the quote.

@LostKobrakai thank you! I tried that but then it executes the Gettext.dgettext/3 three times and I receive a triple string back!

defmodule DemoWeb.Domains do
  require Gettext.Macros

  defmacro dtext(domain, message) do
    for d <- ["default", "domain1", "domain2"] do
      quote do
        Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, unquote(d), unquote(message))

        Gettext.dgettext(DemoWeb.Gettext, unquote(domain), unquote(message))
      end
    end
  end
end

# page.html.heex
{dtext(Enum.random(["default", "domain1", "domain2"]), "Hello World!")}
=> "Hello World!Hello World!Hello World!"
noops = 
    for d <- ["default", "domain1", "domain2"] do
      quote do
        Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, unquote(d), unquote(message))
      end
    end 

quote do
  unquote(noops)
  Gettext.dgettext(DemoWeb.Gettext, unquote(domain), unquote(message))
end

I’d always suggest writing the code you want to generate first, then try to write the macro that is meant to generate that code.

2 Likes

Heck yeah! That worked! Thank you so much! :heart:

Just for completeness sake, here’s my solution:

defmodule DemoWeb.Domains do
  require Gettext.Macros

  defmacro dtext(domain, message) do
    noops =
      for d <- ["default", "domain1", "domain2"] do
        quote do
          Gettext.Macros.dgettext_noop_with_backend(DemoWeb.Gettext, unquote(d), unquote(message))
        end
      end

    quote do
      unquote(noops)
      Gettext.dgettext(DemoWeb.Gettext, unquote(domain), unquote(message))
    end
  end
end

3 Likes