Gettext domains with code generation

I’m trying to create some translations for my CMS resources, and I’m trying to automatically create Gettext domains for each resource.

defmodule MyApp.Projects.Project do
  use B.Blueprint
  import MyApp.Gettext

  @singular "project"
  @plural "projects"

  # these macros basically build a map of translations
  translations do
    context :fields do
      translate :title do
        label t("Some label text")
        placeholder t("Some placeholder")
      end
    end
  end
end

The t macro lives in the used B.Blueprint and looks like this:

 # ...
  # 
  defmacro t(msgid) do
    quote do
      dgettext(@plural, msgid)
    end
  end

This doesn’t work of course:

** (ArgumentError) Gettext macros expect translation 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:

{{:., [], [{:__aliases__, [counter: {MyApp.Projects.Project, 58}, alias: false], [:Module]}, :__get_attribute__]}, [], [{:__MODULE__, [counter: {MyApp.Projects.Project, 58}], Kernel}, :plural, 83]}

Is this doable in any way? I was hoping to not having to put all my translations in the “default.pot” file, and also not having to do t("projects", "msg here") :slight_smile:

Something like this should do the trick however for some reason Module.get_attribute/2 is returning nil which I did not expect. My early morning brain isn’t resolving why so putting here so smarter brains can clarify.

defmodule B.Blueprint do
  defmacro t(msgid) do
    caller = __CALLER__.module

    # On my quick test this is returning `nil` which surprised me
    domain = Module.get_attribute(caller, :plural)

    # Testing this way works
    domain = "plural"

    quote do
      dgettext(unquote(domain), unquote(msgid))
    end
  end

end

defmodule MyApp.Projects.Project do
  import MyApp.Gettext
  import B.Blueprint

  @plural "projects"

  t("some message")
end
1 Like

Thanks for the __CALLER__.module hint!

I did some logging and it seems that the t macro is run before the module attribute is set, so I had to do a slight API adjustment:

defmodule MyApp.Projects.Project do
  use B.Blueprint, 
    singular: "Project",
    plural: "Projects"

  import MyApp.Gettext

  # these macros basically build a map of translations
  translations do
    context :fields do
      translate :title do
        label t("Some label text")
        placeholder t("Some placeholder")
      end
    end
  end
end

then in B.Blueprint

  defmacro __using__(opts) do
    Module.register_attribute(__CALLER__.module, :singular, accumulate: false)
    Module.put_attribute(__CALLER__.module, :singular, Keyword.fetch!(opts, :singular))

    Module.register_attribute(__CALLER__.module, :plural, accumulate: false)
    Module.put_attribute(__CALLER__.module, :plural, Keyword.fetch!(opts, :plural))
    # ...

Now the module attribute is available in my t macro! :tada:

Cool, I think thats a better and more obvious API too. Glad its up and running for you!

1 Like

I’m having the same wish you do. But I’m failing to get this to work.

What is this B.Blueprint? And where does that code live?

I’ve tried to make a translations.ex (in the same location as utils.ex)

defmodule Translations do
  defmacro tr(msgid) do
    domain = "plural"

    quote do
      dgettext(unquote(domain), unquote(msgid))
    end
  end

  defmacro __using__(opts) do
    Module.register_attribute(__CALLER__.module, :singular, accumulate: false)
    Module.put_attribute(__CALLER__.module, :singular, Keyword.fetch!(opts, :singular))

    Module.register_attribute(__CALLER__.module, :plural, accumulate: false)
    Module.put_attribute(__CALLER__.module, :plural, Keyword.fetch!(opts, :plural))
  end

end

But using it proves difficult :frowning:

use Translations, plural: "menus", singular: "menu"

def public_menu_items(_user \\ nil),
    do: [
      %{label: tr("Features"), path: "/#features"},
    ]
end

== Compilation error in file lib/cavalato_web/menus.ex ==
** (CompileError) lib/cavalato_web/menus.ex:13: undefined function tr/1 (expected CavalatoWeb.Menus to define such a function or for it to be imported, but none are available)

Obviously, when doing this it doesn’t crash, but it also doesn’t do the translating :thinking:

use Translations, plural: "menus", singular: "menu"

def public_menu_items(_user \\ nil),
    do: [
      %{label: Translations.tr("Features"), path: "/#features"},
    ]
end