Translating Phoenix Applications With Gettext

Phoenix localization

Internationalization is a very common task when creating a web application and I have already covered how to achieve this task in Ruby on Rails, pure Ruby, and JavaScript applications in this Blog. Today, however, I'd like to talk about a slightly less popular but very promising framework called Phoenix.

Phoenix is a fast and reliable MVC framework written in the language Elixir (which, in turn, relies on Erlang). It has many features that should be familiar to developers who come from the Rails or Django world, but, at the same time, it may seem a bit complex at first due to Elixir’s functional nature.

In this article, you will learn about Phoenix i18n. I’ll walk you through how to add support for i18n in Phoenix applications with the help of Gettext (which is a default dependency). You will learn what Gettext is, what PO and POT files are, how to generate them and easily extract translations from your views. I will also talk about supporting multiple locales, pluralization rules, and domains. If you would like to run the code samples presented in this article locally, you’ll need to install OTP (at least 18), Elixir (at least 1.4) and, of course, Phoenix framework itself (version 1.3 will be used in this tutorial).

The source code for this article can be found on GitHub.


So, Gettext is a complex open source solution created by GNU (initially it was introduced by Sun Microsystems in the middle 90s). It is used to create multilingual systems (not only web applications) by many developers and companies, so you may find lots of materials about it on the net. Discussing all features of Gettext is outside of the scope of this article, but you may find full documentation online. We will be mainly interested in how Gettext files should be named, organized and what they are used for.

Gettext instructs us to create a folder named after the locales they are going to support. For example, en, ru, de etc. Inside, there should be a folder called LC_MESSAGES with one or multiple .po files. PO means “portable object” and these files contain strings to be translated as well as the actual translations. So, the file structure should look like this:

  • en
      • default.po
      • other.po
  • ru
      • default.po
      • other.po

default and other are names of the domains (or scopes) and I will cover them later in this article.

Apart from PO files, there are also POT files (which stands for “portable object template”). They are used as a base to create PO files and soon you will see why it matters. I think we can already proceed to the main part of the article because it will be much easier to understand everything in practice.

Simple Translations

Make sure you have Erlang, Elixir and Phoenix installed on your machine and create a new application by running:

We are using a --no-ecto flag here because the database won’t be needed for the purposes of this article. The installer will ask whether you’d like to install dependencies, so type Y and hit Enter. After a couple of minutes, your new shiny application will be prepared!

Open the demo/lib/demo_web/templates/page/index.html.eex file. This is a default starting page for the application. Remove everything except for this block:

It is a welcoming message, but note that it utilizes a gettext function — this is exactly how we are going to display translated texts to our users. So what is going on here? gettext accepts at least one argument: that is, a string that we would like to translate. You may call it a “key” for now, but strictly speaking, it is more than just a key. You see, when adding support for i18nin frameworks like Ruby on Rails we usually have to provide keys in a format of top_scope.other_scope.my_key. For example, in Rails that would be:

Next, in a separate file, we would need to provide a translation for this key. If the translation is missing, the key itself would be rendered on the screen in a more or less prettified way. Gettext uses a different approach where the strings to localize act like keys themselves. So, even if the translation cannot be found, the string can be printed on the screen. This is also convenient because if you need to internationalize an existing application, the strings can be simply passed to the gettext function (of course, in some cases you will need to do more work).

The second argument passed to gettext, as you’ve probably guessed, contains parameters (or bindings) that we want to interpolate into the translation. In our case, we have one parameter called name. Note that all the parameters must be wrapped with %{}.

To make sure that the string actually does work, cd into the folder containing your project, boot the server

and navigate to http://localhost:4000. You will see the “Welcome to Phoenix!” phrase which means everything is working as expected!

Adding a New Translation

For demonstration purposes let’s also add a subtitle to our root page:

Now, where can we add a translation for this new string? Well, all translations are stored under the priv/gettext folder that already has some default files inside. We can, of course, create all the necessary files manually, but that would be too tedious. Instead, run the following command:

It is going to scan the project’s files and check if Gettext is used anywhere. A new priv/gettext/default.pot file will be created for you. As already mentioned above, .pot means “portable object template” and so this file is used as a template to generate translations for other languages. Note that this file should not be modified directly. If you open it now, you’ll see something like that:

Note that our messages were added here automatically and you can even see the lines where they are located. msgid is the “key” and msgstr contains the translation. Really cool!

Now we need to generate a PO translation file for the English language, so run another command:

This command, basically, utilizes the default.pot template and creates a default.po file in the priv/gettext/en/LC_MESSAGES folder. You may also run:

to create (or update) the template and .po files in one go.

The default.po file has the following contents:

Here you may translate the given strings as needed. Of course, this makes little sense to do so because the messages are already in English, but let’s do the following tweak to make sure everything is still working fine:

If you visit the root page of the website now, you should see “Howdy from Phoenix!” which means our translation was used properly. Good job!

Multiple Locales

So, the default locale for Phoenix apps is en (English). You may change this setting by tweaking the config/config.exs file:

Now the default locale is Russian, but you may specify anything else. Let’s also provide the list of supported locales for our application:

By the way, you may get a list of all known locales in the following way:

Next, we need to generate PO files for the Russian locale, so run:

This is going to generate a priv/gettext/ru/LC_MESSAGES folder with .po files inside. Tweak the default.po by adding some translations:

Now, depending on the chosen locale, Phoenix will render either English or Russian translations. If you need to enforce a locale, you may use the with_locale function:

Switching Between Locales

Alright, the translations are added, but how do we understand which locale the user would like to utilize? One of the easiest ways to achieve this task is by using the set_locale plug that extracts the desired locale from URLs or Accept-Language HTTP header. To specify a locale in the URL, one would type http://localhost:4000/en/some_path. If the locale is not specified (or if an unsupported language was requested), one of two things will happen:

  • If the request contains an Accept-Language HTTP header and this locale is supported, the user will be redirected to a page with the corresponding locale.
  • Otherwise, the user will be automatically redirected to a URL that contains the code of the default locale (Russian, in our case).

The plug is really easy to work with. Open the  mix.exs file and add set_locale to the deps function:

Also, add it to the application:

Next run:

to install everything.

The lib/demo_web/router.ex needs some changes as well. Add a new plug to the :browser pipeline:

Lastly, create a new scope:

This is it. Now boot the server and try navigating to http://localhost:4000/ru and http://localhost:4000/en. You should see that the messages are translated properly which is exactly what we need!


Suppose we’d like to say how many messages the user has. It does not really matter what messages we are talking about or where they come from — what matters is the fact that there can be 1 or more messages. It means we need to introduce a pluralization rule. Luckily, the Gettext plug already takes care of that for us by introducing the ngettext function. This function accepts at least 3 arguments: a string in singular form, a string in plural form, and count. You may also provide so-called bindings (other parameters that should be interpolated into the translation).

So, let’s utilize this function now:

%{count} here is an interpolation that will be replaced with some number (2 in this case). Now update our template and PO files:

A new entry will be added to the default.po files:

msgstr[0] should contain the text that will be displayed when there is only 1 message. msgstr[1], of course, stores the text for the case when there are multiple messages. This is totally okay for the English locale, but not enough for Russian. This language has more complex pluralization rules, but most existing languages are already supported by the Gettext.Plural behavior. So, let’s cover all the possible cases for the Russian locale:

Now, for 1 message we’ll use case 0, for zero or few messages — case 1, and case 2 in all other situations.

Working With Domains

All translations added so far were placed to the default.po files. default here is the name of the domain that acts as a scope or a namespace. Having only one scope may be quite okay for a small application, but this is not very convenient for large systems that have hundreds of translations. Gettext does support multiple domains and so let’s try to introduce one by using the dgettext function:

This function is very similar to gettext but accepts the domain name as the first argument. In our case we’ve specified system to pretend it is going to contain some service messages. Now run:

You will note that system.pot and two system.po files will be created. So, these files should be named after the domain and contain only the messages that belong to this domain. You may add translation inside the priv/ru/LC_MESSAGES/system.po:

Note that if you’d like to grab a pluralized translation from a specific domain, a dngettext function should be used:

Stick with Phrase!

Writing code to localize your application is one task, but working with translations is a something else. Having plenty of translations for multiple languages may quickly overwhelm you which will lead to the user’s confusion. Luckily, Phrase can make your life as a developer a lot easier!

Feel free to grab your 14-day trial now. Phrase supports all of your formats, including, of course, PO. It allows to easily import and export translation data and search for any missing translations, which is really convenient. On top of that, you can collaborate with translators as it is much better to have professionally done localization for your website. If you’d like to learn more about Phrase, refer to the Getting Started guide.


In this article, we have seen how to translate a Phoenix application with the help of Gettext. We have discussed what Gettext is, what PO and POT files are, how to generate them and extract all the necessary strings from the application. You have learned how to add support for multiple locales and set the proper locale based on the user’s preferences. Also, we have talked about utilizing pluralization rules and working with domains.

If you would like to learn more about Gettext in the Phoenix framework, you may refer to this official guide on that provides useful examples and API reference for all the available functions. I hope this article was useful and interesting! Thanks for staying with me today and until the next time.

2.8 (55.14%) 37 votes