Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Translating Phoenix Applications With Gettext

DZone's Guide to

Translating Phoenix Applications With Gettext

In this post, we look at how to create a localized app using the Phoenix framework with the Gettext localization tool to make your site more user friendly.

· Web Dev Zone ·
Free Resource

Jumpstart your Angular applications with Indigo.Design, a unified platform for visual design, UX prototyping, code generation, and app development.

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, the Phoenix framework itself (version 1.3 will be used in this tutorial).

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

Gettext?

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 the 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
    • LC_MESSAGES
      • default.po
      • other.po
  • ru
    • LC_MESSAGES
      • 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:

mix phx.new demo --no-ecto

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:

<div class="jumbotron">
  <h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2>
</div>

It is a welcome 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 i18n in 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:

I18n.translate 'users.messages.welcome'

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, and boot up the server:

mix phx.server

Then 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:

<div class="jumbotron">
  <h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2>
  <h3><%= gettext "This tutorial is brought to you by %{company}",
  company: "PhraseApp" %></h3>
</div>

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:

mix gettext.extract

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 this:

## 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 "This tutorial is brought to you by %{company}"
msgstr ""

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

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:

mix gettext.merge priv/gettext

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:

mix gettext.extract --merge

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

The default.po file 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: en\n"

#: lib/demo_web/templates/page/index.html.eex:3
msgid "This tutorial is brought to you by %{company}"
msgstr ""

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

Here, you may translate the given strings as needed. Of course, it 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:

msgid "Welcome to %{name}!"
msgstr "Howdy from %{name}!"

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:

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

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

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

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

Gettext.known_locales(MyApp.Gettext) #=> ["en", "ru"]

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

mix gettext.merge priv/gettext --locale ru

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

#: lib/demo_web/templates/page/index.html.eex:3
msgid "This tutorial is brought to you by %{company}"
msgstr "Руководство от компании %{company}"

#: 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. If you need to enforce a locale, you may use the with_locale function:

Gettext.with_locale DemoWeb.Gettext, "en", fn ->
  MyApp.DemoWeb.gettext("test string")
end

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:

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

Also, add it to the application:

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

Next run:

mix deps.get

This will install everything.

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

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

Lastly, create a new scope:

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

    get "/", PageController, :index
  end

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!

Pluralization

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:

<p>
  <%= ngettext "You have one message", "You have %{count} messages", 2 %>
</p>

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

mix gettext.extract --merge

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

msgid "You have one message"
msgid_plural "You have %{count} messages"
msgstr[0] ""
msgstr[1] ""

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:

msgid "You have one message"
msgid_plural "You have %{count} messages"
msgstr[0] "У вас одно новое сообщение"
msgstr[1] "У вас %{count} новых сообщения"
msgstr[2] "У вас %{count} новых сообщений"

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 into 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:

<p>
  <%= dgettext "system", "Some system output: %{msg}", msg: "debug goes here" %>
</p>

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:

mix gettext.extract --merge

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:

msgid "Some system output: %{msg}"
msgstr "Системное сообщение: %{msg}"

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

dgettext "domain", "Singular %{msg}", "Plural %{msg}", 4, msg: "demo"

Conclusion

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 hexdocs.pm that provides useful examples and API references for all the available functions. I hope this article was useful and interesting! 

Take a look at an Indigo.Design sample application to learn more about how apps are created with design to code software.

Topics:
i18n ,phoenix ,gettext ,internationalization ,web dev

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}