Instructor Lead On-Demand Learning Courses - Learning Extravaganza All Pro Web Designs and Udemy are happy to offer this special to you. Act Now!

Phoenix I18n

In my previous articles I covered the various aspects of Elixir—a modern functional programming language. Today, however, I would like to step aside from the language itself and discuss a very fast and reliable MVC framework called Phoenix that is written in Elixir.

This framework emerged nearly five years ago and has received some traction since then. Of course, it is not as popular as Rails or Django yet, but it does have great potential and I really like it.

In this article we are going to see how to introduce I18n in Phoenix applications. What is I18n, you ask? Well, it is a numeronym that means “internationalization”, as there are exactly 18 characters between the first letter “i” and the last “n”. Probably, you have also met an L10n numeronym which means “localization”. Developers these days are so lazy they can’t even write a couple of extra characters, eh?

Internationalization is a very important process, especially if you foresee the application being used by people from all around the world. After all, not everyone knows English well, and having the app translated into a user’s native language gives a good impression.

It appears that the process of translating Phoenix applications is somewhat different from, say, translating Rails apps (but quite similar to the same process in Django). To translate Phoenix applications, we use quite a popular solution called Gettext, which has been around for more than 25 years already. Gettext works with special types of files, namely PO and POT, and supports features like scoping, pluralization, and other goodies. 

Instructor Lead On-Demand Learning Courses All Pro Web Designs and Udemy are happy to offer this special to you. Act Now!

So in this post I am going to explain to you what Gettext is, how PO differs from POT, how to localize messages in Phoenix, and where to store translations. Also we are going to see how to switch the application’s locale and how to work with pluralization rules and domains.

Shall we start?

Internationalization With Gettext

Gettext is a battle-tested open-source internationalization tool initially introduced by Sun Microsystems in 1990. In 1995, GNU released its own version of Gettext, which is now considered to be the most popular out there (the latest version was 0.19.8 at the time of writing this article). Gettext may be used to create multilingual systems of any size and type, from web apps to operational systems. This solution is quite complex, and we are not going to discuss all its features, of course. The full Gettext documentation can be found at

Gettext provides you all the necessary tools to perform localization and presents some requirements on how translation files should be named and organized. Two file types are used to host translations: PO and MO.

PO (Portable Object) files store translations for given strings as well as pluralization rules and metadata. These files have quite a simple structure and can be easily edited by a human, so in this article we will stick to them. Each PO file contains translations (or part of the translations) for a single language and should be stored in a
directory named after this language: en, fr, de,

MO (Machine Object) files contain binary data not meant to be edited directly by a human. They are harder to work with, and discussing them is out of the scope of this article.

To make things more complex, there are also POT (Portable Object Template) files. They host only strings of data to translate, but not the translations themselves. Basically, POT files are used only as blueprints to create PO files for various locales.

Sample Phoenix Application

Okay, so now let’s proceed to practice! If you’d like to follow along, make sure you have installed the following:

  • OTP (version 18 or higher)
  • Elixir (1.4+)
  • Phoenix framework (I’m going to be using version 1.3)

Create a new sample application without a database by running:

mix i18ndemo --no-ecto

--no-ecto says that the database should not be utilized by the app (Ecto is a tool to communicate with the DB itself). Note that the generator might require a couple of minutes to prepare everything.

Now use cd to go to the newly created i18ndemo folder and run the following command to boot the server:

mix phx.server

Next, open the browser and navigate to http://localhost:4000, where you should see a “Welcome to Phoenix!” message.

Hello, Gettext!

What’s interesting about our Phoenix app and, specifically, the welcoming message is that Gettext is already being used by default. Go ahead and open the demo/lib/demo_web/templates/page/index.html.eex file which acts as a default starting page. Remove everything except for this code:

< %= gettext "Welcome to %{name}!", name: "Phoenix" %>

This welcoming message utilizes a gettext function which accepts a string to translate as the first argument. This string can be considered as a translation key, though it is somewhat different from the keys used in Rails I18n and some other frameworks. In Rails we would have used a key like page.welcome, whereas here the translated string is a key itself. So, if the translation cannot be found, we can display this string directly. Even a user who knows English poorly can get at least a basic sense of what’s going on.

This approach is quite handy actually—stop for a second and think about it. You have an application where all messages are in English. If you’d like to internationalize it, in the simplest case all you have to do is wrap your messages with the gettext function and provide translations for them (later we will see that the process of extracting the keys can be easily automated, which speeds things up even more).

Okay, let’s return to our small code snippet and take a look at the second argument passed to gettext: name: "Phoenix". This is a so-called binding—a parameter wrapped with %{} that we’d like to interpolate into the given translation. In this example, there is only one parameter called name.

We can also add one more message to this page for demonstration purposes: 

< %= gettext "Welcome to %{name}!", name: "Phoenix" %>

< %= gettext "We are using version %{version}", version: "1.3" %>

Adding a New Translation

Now that we have two messages on the root page, where should we add translations for them? It appears that all translations are stored under the priv/gettext folder, which has a predefined structure. Let’s take a moment to discuss how Gettext files should be organized (this applies not only to Phoenix but to any app using Gettext).

First of all, we should create a folder named after the locale it is going to store translations for. Inside, there should be a folder called LC_MESSAGES containing one or multiple .po files with the actual translations. In the simplest case, you’d have one default.po file per locale. default here is the domain’s (or scope’s) name. Domains are used to divide translations into various groups: for example, you might have domains named admin, wysiwig, cart, and other. This is convenient when you have a large application with hundreds of messages. For smaller apps, however, having a sole default domain is enough. 

So our file structure might look like this:

  • en
      • default.po
      • admin.po
  • ru
      • default.po
      • admin.po

To starting creating PO files, we first need the corresponding template (POT). We can create it manually, but I’m too lazy to do it this way. Let’s run the following command instead:

mix gettext.extract

It is a very handy tool that scans the project’s files and checks whether Gettext is used anywhere. After the script finishes its job, a new priv/gettext/default.pot file containing strings to translate will be created.

As we’ve already learned, POT files are templates, so they store only the keys themselves, not the translations, so do not modify such files manually. Open a newly created file and take a look at its contents:

## This file is a PO Template file.
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
msgid ""
msgstr ""

#: lib/demo_web/templates/page/index.html.eex:3
msgid "We are using version %{version}"
msgstr ""

#: lib/demo_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr ""

Convenient, isn’t it? All our messages were inserted automatically, and we can easily see exactly where they are located. msgid, as you’ve probably guessed, is the key, whereas msgstr is going to contain a translation.

The next step is, of course, generating a PO file. Run:

mix gettext.merge priv/gettext

This script is going to utilize the default.pot template and create a default.po file in the priv/gettext/en/LC_MESSAGES folder.  For now, we have only an English locale, but support for another language will be added in the next section as well.

By the way, it is possible to create or update the POT template and all PO files in one go by using the following command:

mix gettext.extract --merge

Now let’s open the priv/gettext/en/LC_MESSAGES/default.po file, which has the following contents:

## `msgid`s in this file come from POT (.pot) files.
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: enn"

#: lib/demo_web/templates/page/index.html.eex:3
msgid "We are using version %{version}"
msgstr ""

#: lib/demo_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr ""

This is the file where we should perform the actual translation. Of course, it makes little sense to do so because the messages are already in English, so let’s proceed to the next section and add support for a second language.

Multiple Locales

Naturally, the default locale for Phoenix applications is English, but this setting can be changed easily by tweaking the config/config.exs file. For example, let’s set the default locale to Russian (feel free to stick with any other language of your choice):

config :demo, I18ndemoWeb.Gettext, default_locale: "ru"

It is also a good idea to specify the full list of all supported locales:

config :demo, I18ndemoWeb.Gettext, default_locale: "ru", locales: ~w(en ru)

Now what we need to do is generate a new PO file containing translations for the Russian locale. It can be done by running the gettext.merge script again, but with a --locale switch:

mix gettext.merge priv/gettext --locale ru

Obviously, a priv/gettext/ru/LC_MESSAGES folder with the .po files inside will be generated. Note, by the way, that apart from the default.po file, we also have errors.po. This is a default place to translate error messages, but in this article we are going to ignore it.

Now tweak the priv/gettext/ru/LC_MESSAGES/default.po by adding some translations:

#: lib/demo_web/templates/page/index.html.eex:3
msgid "We are using version %{version}"
msgstr "???????????? ?????? %{version}"

#: lib/demo_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr "????? ?????????? ? ?????????? %{name}!"

Now, depending on the chosen locale, Phoenix will render either English or Russian translations. But hold on! How can we actually switch between locales in our application? Let’s proceed to the next section and find out!

Switching Between Locales

Now that some translations are present, we need to enable our users to switch between locales. It appears that there is a third-party plug for that called set_locale. It works by extracting the chosen locale from the URL or Accept-Language HTTP header. So, to specify a locale in the URL, you 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.

Open the  mix.exs file and drop in set_locale to the deps function:

  defp deps do
      # ...
      {:set_locale, "~> 0.2.1"}

We must also add it to the application function:

  def application do
      mod: {Demo.Application, []},
      extra_applications: [:logger, :runtime_tools, :set_locale]

Next, install everything:

mix deps.get

Our router located at lib/demo_web/router.ex requires some changes as well. Specifically, we need to add a new plug to the :browser pipeline:

  pipeline :browser do
    # ...
    plug SetLocale, gettext: DemoWeb.Gettext, default_locale: "ru"

Also, create a new scope:

  scope "/:locale", DemoWeb do
    pipe_through :browser

    get "/", PageController, :index

And that’s it! You can boot the server and navigate to http://localhost:4000/ru and http://localhost:4000/en. Note that the messages are translated properly, which is exactly what we need!

Alternatively, you may code a similar feature yourself by utilizing a Module plug. A small example can be found in the official Phoenix guide.

One last thing to mention is that in some cases you might need to enforce a specific locale. To do that, simply utilize a with_locale function:

Gettext.with_locale I18ndemoWeb.Gettext, "en", fn ->


We have learned the fundamentals of using Gettext with Phoenix, so the time has come to discuss slightly more complex things. Pluralization is one of them. Basically, working with plural and singular forms is a very common though potentially complex task. Things are more or less obvious in English as you have “1 apple”, “2 apples”, “9000 apples” etc (though “1 ox”, “2 oxen”!).

Unfortunately, in some other languages like Russian or Polish, the rules are more complex. For example, in the case of apples, you’d say “1 ??????”, “2 ??????”, “9000 ?????”. But luckily for us, Phoenix has a Gettext.Plural behavior (you may see the behavior in action in one of my previous articles) that supports many different languages. Therefore all we have to do is take advantage of the ngettext function.

This function accepts three required arguments: a string in singular form, a string in plural form, and count. The fourth argument is optional and can contain bindings that should be interpolated into the translation.

Let’s see ngettext in action by saying how much money the user has by modifying the demo/lib/demo_web/templates/page/index.html.eex file:

< %= ngettext "You have one buck. Ow :(", "You have %{count} bucks", 540 %>

%{count} is an interpolation that will be replaced with a number (540 in this case). Don’t forget to update the template and all PO files after adding the above string:

mix gettext.extract --merge

You will see that a new block was added to both default.po files:

msgid "You have one buck. Ow :("
msgid_plural "You have %{count} bucks"
msgstr[0] ""
msgstr[1] ""

We have not one but two keys here at once: in singular and in plural forms. msgstr[0] is going to contain some text to display when there is only one message. msgstr[1], of course, contains the text to show when there are multiple messages. This is okay for English, but not enough for Russian where we need to introduce a third case: 

msgid "You have one buck. Ow :("
msgid_plural "You have %{count} bucks"
msgstr[0] "? 1 ??????. ???????? ?????!"
msgstr[1] "? ??? %{count} ???????"
msgstr[2] "? ??? %{count} ????????"

Case 0 is used for 1 buck, and case 1 for zero or few bucks. Case 2 is used otherwise.

Scoping Translations With Domains

Another topic that I wanted to discuss in this article is devoted to domains. As we already know, domains are used to scope translations, mainly in large applications. Basically, they act like namespaces.

After all, you may end up in a situation when the same key is used in multiple places, but should be translated a bit differently. Or when you have way too many translations in a single default.po file and would like to split them somehow. That’s when domains can come in really handy. 

Gettext supports multiple domains out of the box. All you have to do is utilize the dgettext function, which works nearly the same as gettext. The only difference is that it accepts the domain name as the first argument. For instance, let’s introduce a notification domain to, well, display notifications. Add three more lines of code to the demo/lib/demo_web/templates/page/index.html.eex file:

< %= dgettext "notifications", "Heads up: %{msg}", msg: "something has happened!" %>

Now we need to create new POT and PO files:

mix gettext.extract --merge

After the script finishes doing its job, notifications.pot as well as two notifications.po files will be created. Note once again that they are named after the domain. All you have to do now is add translation for the Russian language by modifying the priv/ru/LC_MESSAGES/notifications.po file:

msgid "Heads up: %{msg}}"
msgstr "????????: %{msg}"

What if you would like to pluralize a message stored under a given domain? This is as simple as utilizing a 
See more from Net Tuts

Instructor Lead On-Demand Learning Courses All Pro Web Designs and Udemy are happy to offer this special to you. Act Now!

Leave a Reply