How to perform localization in Phoenix applications with Gettext

Currently we do not have any mechanism to switch the language, so set Russian as a default locale:# config/config.

exs config :lokalise_demo, LokaliseDemoWeb.

Gettext, locales: ~w(en ru), default_locale: "ru" # <== modify this lineNow start the server by running:mix phx.

serverOpen http://localhost:4000 page in your browser and make sure that the translated message is shown!Gettext PluralizationAnother important feature that I would like to cover is the pluralization.

Different languages have different pluralization rules, and Gettext supports many of them out of the box.

Still, it is our job to provide proper translations for all potential cases.

As a very simple example, let’s say how many apples the user has.

Suppose we don’t know the exact amount, which means that the sentence may read as “1 apple” or “X apples”.

To support pluralization, we have to stick with the ngettext/5 function:ngettext "You have 1 apple", "You have %{count} apples", 2This function accepts both singular and plural forms of the sentence, as well as the count.

Under the hood, Gettext takes this count and chooses the proper translation based on the pluralization rules.

Next you may update the POT and PO files with the following commands:mix gettext.

extract –merge priv/gettext mix gettext.

extract –merge priv/gettext –locale=ruYou’ll find a couple of new lines inside the Gettext files:msgid "You have 1 apple" msgid_plural "You have %{count} apples" msgstr[0] "" msgstr[1] ""msgstr[0] and msgstr[1] contain translations for singular and plural forms respectively.

For English we don’t need to do anything else, but the Russian language requires some extra steps:msgid "You have one message" msgid_plural "You have %{count} messages" msgstr[0] "У вас одно яблоко" msgstr[1] "У вас %{count} яблока" msgstr[2] "У вас %{count} яблок"The pluralization rules in this case are a bit more complex, therefore we must provide not two, but three possible options.

You may find more information on the topic in the official docs.

Choosing The App’s LocaleAs I already mentioned earlier, currently there is no way to actually switch between locales when browsing the app.

This is an important feature, so let’s add it now!All in all, we have two potential solutions:Utilize a third-party solution, for example the set_locale plug (the easy way)Write everything from scratch (the warrior’s way)If you choose to stick with the third-party plug, things will be very simple indeed.

You need to perform only three quick steps:Install the packageAdd a new plug to the router.

ex fileAdd a new :locale routing scopeAfter that the locale will be inferred from the URL, cookies, or the accept-language request header.

Simple.

However, in this tutorial I propose choosing a more complex way and writing this feature from scratch.

Reading Locale From the URLThe most common way of specifying the desired locale is via the URL.

The language’s code may be a part of the domain name, or a part of the path:http://en.

example.

com/some/pathhttp://example.

com/en/some/pathhttp://example.

com/some/path?locale=enLet’s stick with the latter option and provide the locale as a GET parameter.

To read the locale’s value and do something about it, we need a custom plug.

Create a new lib/lokalise_demo_web/plugs/set_locale_plug.

ex file with the following contents:defmodule LokaliseDemoWeb.

Plugs.

SetLocale do import Plug.

Conn # 1 @supported_locales Gettext.

known_locales(LokaliseDemoWeb.

Gettext) # 2 def init(_options), do: nil # 3 def call(%Plug.

Conn{params: %{"locale" => locale}} = conn, _options) when locale in @supported_locales do # 4 end def call(conn, _options), do: conn # 5 endLet’s discuss this code snippet:On this line we are importing a behavior.

It requires us to fulfill a certain contract (see below).

This is the module attribute with a list of supported localesThis is the actual fulfillment of the contract: a callback that gets invoked automatically.

It may return options passed to the call/2 function, or just nilThe call/2 is initialized with all the GET parameters of the request.

We are only interested in the locale part and fetch it using the pattern matching mechanism.

Also on this line we have a guard clause that ensures the chosen language is actually supportedThis is the fallback clause that gets invoked when the passed locale is unsupported.

In this case we just return the connection without any modifications.

The last thing we need to do is flesh out the first clause of the call/2 function.

It simply has to set the chosen locale as the current one:def call(%Plug.

Conn{params: %{"locale" => locale}} = conn, _options) when locale in @supported_locales do LokaliseDemoWeb.

Gettext |> Gettext.

put_locale(locale) conn endNote that the conn must be returned by the call/2 function!The plug is ready, and you may place it inside the :browser pipeline:# lib/router.

ex # .

pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug LokaliseDemoWeb.

Plugs.

SetLocale endNow reload the server and navigate to http://localhost:4000/?locale=en.

The welcoming message should be in English which means that the custom plug is working as expected!Storing Locale Into a CookieOur next task is persisting the chosen locale among requests so that the user does not need to provide it every time.

The perfect candidate for such persistence would be cookies: small text files stored on the user’s PC.

Phoenix indeed has support for cookies out of the box, so just utilize a put_resp_cookies/4 function inside your plug:def call(%Plug.

Conn{params: %{"locale" => locale}} = conn, _options) when locale in @supported_locales do LokaliseDemoWeb.

Gettext |> Gettext.

put_locale(locale) conn |> put_resp_cookie "locale", locale, max_age: 365*24*60*60 endWe modify the connection by storing a cookie named "locale".

It has a lifetime of 1 year which effectively means eternity in terms of the web.

The last step here is reading the chosen locale from the cookie.

Unfortunately, we cannot use a guard clause for this task anymore, so let’s replace two clauses of the call/2 function with only one:def call(conn, _options) do case fetch_locale_from(conn) do nil -> conn locale -> LokaliseDemoWeb.

Gettext |> Gettext.

put_locale(locale) conn |> put_resp_cookie "locale", locale, max_age: 365*24*60*60 end endAll in all, the logic remains the same: we fetch the locale, check it, and then either do nothing or store it as the current one.

Add two private functions to finalize this feature:defp fetch_locale_from(conn) do (conn.

params["locale"] || conn.

cookies["locale"]) |> check_locale end defp check_locale(locale) when locale in @supported_locales, do: locale defp check_locale(_), do: nilHere we are reading the locale from the either the GET param or cookie, and then checking if the desired language is supported.

Then either return this language’s code, or just nil.

Great job!Another pretty common way of setting the locale is by using the Accept-Language HTTP header.

If you would like to implement this mechanism, try utilizing the code from the set_locale plug that already provides all the necessary RegExs and other fancy stuff.

Locale Switcher ControlSo, the SetLocale plug is ready, but we still have not provided any controls to choose the website’s language.

Therefore, let’s render two links at the top of the page.

Define a new helper inside the lib/views/layout_view.

ex file:defmodule LokaliseDemoWeb.

LayoutView do use LokaliseDemoWeb, :view def new_locale(conn, locale, language_title) do "<a href="#{page_path(conn, :index, locale: locale)}">#{language_title}</a>" |> raw end endCall this helper from the templates/layout/app.

html.

eex template:<body> <div class="container"> <header class="header"> <%= new_locale @conn, :en, "English" %> <%= new_locale @conn, :ru, "Russian" %> </header> <!– other stuff –> </div> </body>Reload the page and try switching between locales.

Everything should be working just fine, which means that the task is completed!Simplify Your Life With LokaliseBy now you are probably thinking that supporting multiple languages on a big website is probably a pain.

And, honestly, you are right.

Of course, the translations can be namespaced with the help of domains.

But still you must make sure that all the keys are translated for each and every locale.

Luckily, there is a solution to this problem: the Lokalise platform that makes working with the localization files much simpler.

Let me guide you through the initial setup which is nothing complex really.

To get started, grab your free trialCreate a new project, give it some name, and set English as a base languageClick “Upload Language Files”Upload PO files for all your languagesProceed to the project, and edit your translations as neededYou may also contact professional translator to do the job for youNext simply download your PO files back and replace them inside the priv/gettext folderProfit!Lokalise has many more features including support for dozens of platforms and formats, and even the possibility to upload screenshots in order to read texts from them.

So, stick with Lokalise and make your life easier!ConclusionIn today’s tutorial we have seen how to perform localization of Phoenix applications with the help of Gettext.

We have discussed what Gettext is and what goodies it has to offer.

We have seen how to extract translations, generate templates, and create PO files based on these templates.

You have also learned what domains are, and how to introduce support for pluralization.

On top of that, we have successfully created our custom plug to fetch and persist the chosen locale based on the user’s preferences.

Not bad for one article!To learn more about Phoenix I18n, I encourage you to check out the official guide that provides both general explanations, as well as documentation for individual functions.

To learn about the Gettext and its features in more detail, refer to the GNU’s documentation.

And, of course, if you have any questions feel free to post them in the comments!Originally published at blog.

lokalise.

co on September 27, 2018.

.

. More details

Leave a Reply