Most Elixir projects that require internationalization, localization or both use the Gettext library for translating user-facing texts.

The Elixir library is an implementation of the Gettext system that was released in 1990. As opposed to other translation systems, Gettext translations are based on strings that allow you to see the literal texts that will be displayed to the end user.

# You would do this
gettext("Hello! Your account is now active")

# Instead of something like
translate("user.account.active")

Gettext works by generating a number of template and translation files (also called PO files). Explaining how those files work is out of the scope of this article, but if you are interested I strongly recommend you to take a look at the Elixir Gettext docs.

Using Gettext macros

The PO files become very big very quickly as they contain all your application’s translatable strings. The standard and recommended way of using Gettext is to use the provided set of macros because they can automatically sync the strings in your code with PO files.

There is a catch, though:

[…] This, however, imposes a constraint: arguments passed to any of these macros have to be strings at compile time. This means that they have to be string literals or something that expands to a string literal at compile time.

message =
  case request do
    %Request{approved_at: %Date{}} -> "Great! Your request has been approved."
    %Request{rejected_at: %Date{}} -> "Sorry, your request has been rejected."
  end

#=> ** (ArgumentError) msgid must be a string literal
gettext(message)

Using Gettext functions

The alternative is to use the Gettext functions instead of macros. This sadly means that we have to manage the PO files manually, which is a big downside.

message =
  case request do
    %Request{approved_at: %Date{}} -> "Great! Your request has been approved."
    %Request{rejected_at: %Date{}} -> "Sorry, your request has been rejected."
  end

# This works, but we must sync strings between the code and PO files manually.
Gettext.gettext(MyApp.Gettext, message)

Using Gettext macros and functions

Thankfully, the *_noop macros cover this gap. As per the docs:

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.

This combines the best of both worlds:

  1. We use gettext_noop so Gettext knows about our translation strings and manages them automatically in PO files.
  2. Then we call the Gettext function, which accepts runtime values.
message =
  case request do
    %Request{approved_at: %Date{}} ->
      gettext_noop("Great! Your request has been approved.")
    %Request{rejected_at: %Date{}} ->
      gettext_noop("Sorry, your request has been rejected.")
  end

# We can use runtime values and let Gettext manage PO files automatically.
Gettext.gettext(MyApp.Gettext, message)

Conclusion

Translation files get big really fast and, ideally, we want to delegate the burden of keeping the code and translation files in sync to Gettext.

In some cases we need to show different messages depending on business rules, and we want those messages to be translated just like the rest of the application. For such cases we can combine the Gettext _noop macros with the Gettext functions, which allow us to translate dynamic messages without losing the automatic sync between code and PO files.