When I wrote a Phoenix app for the first time, I quickly asked myself how to organize my translation files. By default, all translations goes into priv/gettext/[LOCALE]/LC_MESSAGES/default.po and it can easily become a big mess. So I started to search for a way to split my translations by domains. It’s not a thing that is explained in the (really good) Phoenix guides.

Under the hood there’s gettext

It took me some time to figure out how to do it the right way and that’s mainly because I was searching through the Phoenix documentation when the real answer was available in the underlying lib that handle translations gettext and its Elixir bridge.

Gettext is used for translation since forever in free software world. It’s a battle tested library that has everything you need when it comes to translations. It can do simple translations, handle plural translations and domain-based translations.

Coming from Ruby and Rails world I’m really used to the ecosystem specific solution (a.k.a i18n gem and YAML files) to handle translation.

To be honest, when I started using Rails, I was wondering why the community wasn’t using gettext since it was the most standard i18n library I was aware of. It was used everywhere.

When I saw that Phoenix was using gettext I got a mixed feeling of “OMG back to this old lib” and “Yeah this good old gettext!”.

After using it a little bit I quickly told to myself “why do we reinvent the wheel when there’s something that good out there?”.

The format is easy to learn and there’s a bunch of tools available to translate strings.

Basic usage of gettext in Phoenix

gettext "Title" searches for a key (msgid) named Title in the default namespace.

So in your .eex file you’ll get something like:

  <th colspan="2"><%= gettext "Status" %></th>
  <th><%= gettext "Title" %></th>
  <th><%= gettext "Brand" %></th>
  <th><%= gettext "Description" %></th>

where gettext will search in the default namespace (default.po) for msgid Status, Title, Brand, and Description. If no translation is found for the current language then the string will be used as is.

To handle translation you have files with two extensions. The first one is .pot (e.g. priv/gettext/default.pot), one per domain, which is generated by scanning the app code and lists all keys. You then have one .po file per locale / domain (e.g. priv/gettext/fr/LC_MESSAGES/default.po), this is where you’ll actually put the translations.

Custom domain

I’m pretty ashamed of having searched so much to find out how to use custom domains / files for translations when the answer was pretty much in the dgettext function.

Gettext.dgettext(Api.Gettext, "additionals", "In progress")

The first argument is the backend used for translation. In a typical Phoenix app it’ll be YourApp.Gettext.

The second argument is the domain is which gettext will search. In our example it means that the translations will be in priv/gettext/additionals.pot and priv/gettext/[locale]/LC_MESSAGES/additionals.po files. Yes the domain is determined by the file name.

The third and last argument is the msgid or in other words the key and untranslated text.

Final words

I think that if your app becomes big enough, it’s a good strategy to divide your translations in multiple contextualized files to avoid collisions and ease translation process.

Hope you’ll find this useful.

Have comments or want to discuss this topic?

Send an email to ~bounga/public-inbox@lists.sr.ht