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.
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
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
<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:
We can see this made a
default.pot file. Let's look at it:
## 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 ""
.pot file is a translation template. The translations themselves go into
.po files. To add translations for english, we can merge our
and we'll get a shell:
mix gettext.merge priv/gettext
Now we can open up the english translation and modify it:
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:
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:
And with that, we can run it again:
If we visit the site, everything is normal:
But if we add a param to specify our locale, we get our Gettext-specified translations instead:
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!
- Gettext on Wikipedia
- elixir-lang/gettext - The Elixir implementation of Gettext.
- i18n in Phoenix apps - The blog post that Rebecca Skinner wrote that I based all of this on.
- Upgrading to Phoenix 1.1
- Upgrading ElixirStatus to Phoenix 1.1
- Repos become supervisors instead of workers in Phoenix 1.1