Today we're going to prepare ElixirStatus for internationalization with Gettext. Gettext is an established internationalization and localization system, and now there's a very good Elixir library for implementing Gettext in our applications. What's more, Phoenix 1.1 supports it by default. Let's have a look at implementing it in ElixirStatus.

Project

I've upgraded ElixirStatus to Phoenix 1.1, and the Pull Request is in the resources section if you'd like to see what that entails. Now that that's in place, let's look at adding Gettext support to it. I've tagged my fork of the repo with before_episode_217 if you wanted to follow along.

The majority of the static text on this site lives in the shared templates, so we'll just internationalize the sidebar for now. Let's open up web/templates/shared/sidebar.html.eex:

<div class="sidebar">
  <div class="container">
    <div class="sidebar-header">
      <a href="/"><img src="<%= static_path(@conn, "/images/logo.png") %>" class="logo"/></a>
      <h1><a href="/">elixirstatus</a></h1>
      <p><%= gettext "Announce your new project, blog post or version update." %></p>
    </div>
  </div>
  <div class="container sidebar-nav-container">
    <label for="sidebar-checkbox" class="sidebar-toggle"></label>

    <!-- Target for toggling the sidebar `.sidebar-checkbox` is for regular
     styles, `#sidebar-checkbox` for behavior. -->
    <input type="checkbox" class="sidebar-checkbox" id="sidebar-checkbox">

    <nav class="sidebar-nav">
      <a class="sidebar-nav-item active" href="/"><%= gettext "Home" %></a>
      <a class="sidebar-nav-item" href="<%= page_path(@conn, :about) %>"><%= gettext "About" %></a>
      <span class="sidebar-nav-item"><%= gettext "You can follow via" %></span>
      <a class="sidebar-nav-item" href="https://twitter.com/elixirstatus" target="_blank">Twitter</a>
      <a class="sidebar-nav-item" href="/rss">RSS</a>
      <span class="sidebar-nav-item"><%= gettext "Running on Phoenix!" %></span>
      <a class="sidebar-nav-item" href="https://github.com/rrrene/elixirstatus-web" target="_blank"><%= gettext "Open Source" %></a>
    </nav>
  </div>
</div>

Now we'll just extract these strings:

mix gettext.extract

We can see this made a default.pot file. Let's look at it:

vim priv/gettext/default.pot
## 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.
#: web/views/shared_view.ex:18
msgid "About"
msgstr ""

#: web/views/shared_view.ex:6
msgid "Announce your new project, blog post or version update."
msgstr ""

#: web/views/shared_view.ex:17
msgid "Home"
msgstr ""

#: web/views/shared_view.ex:23
msgid "Open Source"
msgstr ""

#: web/views/shared_view.ex:22
msgid "Running on Phoenix!"
msgstr ""

#: web/views/shared_view.ex:19
msgid "You can follow via"
msgstr ""

This .pot file is a translation template. The translations themselves go into the .po files. To add translations for english, we can merge our .pot file and we'll get a shell:

mix gettext.merge priv/gettext

Now we can open up the english translation and modify it:

vim priv/gettext/en/LC_MESSAGES/default.po

However, since the source strings are in English, there's no point in doing that. What we will want to do is add translations for new languages. Now, I'm pretty terrible at languages. I learned French in high school and subsequently forgot it, but we'll give this a shot using the power of Google Translate and my inability to feel shame at doing silly things in public. Let's add a french translation for ElixirStatus and assume that the Open Source community will make it much better in short order.

To add a translation, you just add the locale:

mix gettext.merge priv/gettext --locale fr
vim priv/gettext/fr/LC_MESSAGES/default.po

Alright, let's try this I guess :) I'll paste in my pitiful translations...

## `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: fr\n"

#: web/views/shared_view.ex:18
msgid "About"
msgstr "Concernant"

#: web/views/shared_view.ex:6
msgid "Announce your new project, blog post or version update."
msgstr "Annoncer votre nouveau projet, post de blog ou une version mise à jour."

#: web/views/shared_view.ex:17
msgid "Home"
msgstr "Maison"

#: web/views/shared_view.ex:23
msgid "Open Source"
msgstr "Code Source Libre"

#: web/views/shared_view.ex:22
msgid "Running on Phoenix!"
msgstr "Fonctionnant sur Phoenix!"

#: web/views/shared_view.ex:19
msgid "You can follow via"
msgstr "Vous pouvez suivre via"

So this is almost certainly terrible, but there it is. Next we'll add a plug that lets us switch locales:

vim web/controllers/locale.ex
defmodule ElixirStatus.Locale do
  import Plug.Conn

  def init(opts), do: nil

  def call(conn, _opts) do
    case conn.params["locale"] || get_session(conn, :locale) do
      nil     -> conn
      locale  ->
        Gettext.put_locale(ElixirStatus.Gettext, locale)
        conn |> put_session(:locale, locale)
    end
  end
end

And then we'll add this plug into the pipeline in the router:

    plug ElixirStatus.Locale

And with that, we can run it again:

mix phoenix.server

If we visit the site, everything is normal:

http://localhost:4000

But if we add a param to specify our locale, we get our Gettext-specified translations instead:

http://localhost:4000/?locale=fr

Summary

Et voila! That's how easy it is to internationalize your Elixir applications. There are a host of tools and services that already support working with gettext translation files, so I'm really happy that this was the route that the community took for internationalization. There's a lot of nuance that goes into Gettext, and you can read more about it in the documentation that I've linked in the resources. However, this is enough to get you going. I hope you enjoyed it, and huge thanks to Rebecca Skinner for the blog post that explained its use in Phoenix to me. See you soon!

Resources