{"id":5179,"date":"2016-03-15T12:58:33","date_gmt":"2016-03-15T15:58:33","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=5179"},"modified":"2016-03-15T12:58:59","modified_gmt":"2016-03-15T15:58:59","slug":"using-gettext-to-internationalize-a-phoenix-application","status":"publish","type":"post","link":"http:\/\/blog.plataformatec.com.br\/2016\/03\/using-gettext-to-internationalize-a-phoenix-application\/","title":{"rendered":"Using Gettext to internationalize a Phoenix application"},"content":{"rendered":"
To translate or not to translate? We have been asking ourselves the same question in one of our latest Phoenix projects. Even though internationalizing our application is planned a bit ahead in our roadmap, we have decided to do an initial evaluation of the translation tools in the Elixir ecosystem, and we were pleasantly surprised by what it has to offer.<\/p>\n
The Phoenix framework ships with internationalization (i18n) and localization (l10n) system called Gettext since version 1.1. Gettext<\/strong> is a tool for writing multilingual programs used as a standard by many communities, meaning there is a great set of tooling for developers and translators.<\/p>\n The idea behind Gettext is that it translates messages based on the string itself and not on “keys”. For example, instead of specifying translations as keys, as in We have noticed this approach comes with two large benefits:<\/p>\n We can translate our applications without losing context since we keep the original text. We maintained applications in the past that relied heavily on “custom.message” strings and the extra level of indirection was always hard to work with.<\/p>\n<\/li>\n<\/ul>\n Given those benefits, we have decided to tag already our strings with In this post, we will show you how we did that and how to translate effectively your app when the time comes in the future by running two tasks. Before, let’s take a look at the Gettext structure when we start a new Phoenix project.<\/p>\n Gettext uses two kinds of files: POT<\/strong> means Portable Object Template<\/em> and these files are generated automatically by:<\/p>\n A new Phoenix project already has a The first time you run the task above, it will create a The These files should not<\/strong> be modified manually because this will cause them to be used as a reference for your next files and languages. (well, kind of, as there is a special case that will be covered in another blog post).<\/p>\n PO<\/strong> means Portable Object<\/em> and the files are based on the POT<\/em> files. These are the files that you will use to add your translations. First, we need to generate them with the task:<\/p>\n Now, we have a new PO file in our structure:<\/p>\n Let’s take a look at the new file:<\/p>\n Gettext uses English by default. The Why does my app work when I use Every time that we add a new call to After this, the new Now I want to translate the app to This task will use the template files to generate the What happens if I change my locale to In addition to the simple translation, if you use Your PO file will be generated like:<\/p>\n Interpolation keys can be placed in You can create domains to scope assess your translations. All your translations will be stored in the default domain. Ecto uses a custom domain called One more thing, Gettext stores the locale per-process (in the process dictionary) and per Gettext module. This means that you can use Even after adding You can find more information in the Gettext documentation<\/a>. Do you believe that the To translate or not to translate? We have been asking ourselves the same question in one of our latest Phoenix projects. Even though internationalizing our application is planned a bit ahead in our roadmap, we have decided to do an initial evaluation of the translation tools in the Elixir ecosystem, and we were pleasantly surprised … \u00bb<\/a><\/p>\n","protected":false},"author":18,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[143,245],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5179"}],"collection":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/18"}],"replies":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=5179"}],"version-history":[{"count":17,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5179\/revisions"}],"predecessor-version":[{"id":5197,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5179\/revisions\/5197"}],"wp:attachment":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=5179"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=5179"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=5179"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}Let’s start!<\/h2>\n
translate \"view.welcome\"<\/code>, we simply use the
gettext \"Hello there!\"<\/code> function for every string in the app.<\/p>\n
\n
gettext<\/code> is a single step as it will use the given string if no translation is available;<\/p>\n<\/li>\n
gettext<\/code> calls, saving us from future work without a loss in productivity or maintainability.<\/p>\n
Structure<\/h3>\n
*.po<\/code> and
*.pot<\/code>. These files are stored in
priv\/gettext\/<\/code>. For now, we have only the file errors that are used by Ecto. The structure is:<\/p>\n
priv\/gettext\n\u2514\u2500 en_US\n| \u2514\u2500 LC_MESSAGES\n| \u2514\u2500 errors.po\n\u2514\u2500 errors.pot\n<\/code><\/pre>\n
POT file<\/h3>\n
mix gettext.extract\n<\/code><\/pre>\n
gettext<\/code> call in the
web\/template\/pages\/index.html.eex<\/code> with
<h2><%= gettext \"Welcome to %{name}\", name: \"Phoenix!\" %><\/h2><\/code>.<\/p>\n
default.pot<\/code> file with the text provided:<\/p>\n
priv\/gettext\n\u2514\u2500 en_US\n| \u2514\u2500 LC_MESSAGES\n| \u2514\u2500 errors.po\n\u2514\u2500 default.pot\n\u2514\u2500 errors.pot\n<\/code><\/pre>\n
default.pot<\/code> file is similar to:<\/p>\n
## This file is a PO Template file. `msgid`s here are often extracted from\n## source code; add new translations manually only if they're dynamic\n## translations that can't be statically extracted. Run `mix\n## gettext.extract` to bring this file up to date. Leave `msgstr`s empty as\n## changing them here as no effect; edit them in PO (`.po`) files instead.\n#: web\/templates\/page\/index.html.eex:2\nmsgid \"Welcome to %{name}\"\nmsgstr \"\"\n<\/code><\/pre>\n
PO file<\/h3>\n
mix gettext.merge priv\/gettext\n<\/code><\/pre>\n
priv\/gettext\n\u2514\u2500 en_US\n| \u2514\u2500 LC_MESSAGES\n| \u2514\u2500 default.po\n| \u2514\u2500 errors.po\n\u2514\u2500 default.pot\n\u2514\u2500 errors.pot\n<\/code><\/pre>\n
## `msgid`s in this file come from POT (.pot) files. Do not add, change, or\n## remove `msgid`s manually here as they're tied to the ones in the\n## corresponding POT file (with the same domain). Use `mix gettext.extract\n## --merge` or `mix gettext.merge` to merge POT files into PO files.\nmsgid \"\"\nmsgstr \"\"\n\"Language: en\\n\"\n\n#: web\/templates\/page\/index.html.eex:2\nmsgid \"Welcome to %{name}\"\nmsgstr \"\"\n<\/code><\/pre>\n
msgid<\/code> is the text provided in the
gettext<\/code> function and
msgstr<\/code> is the translated value for it.<\/p>\n
Gettext<\/code> without the translation files? By default, the
gettext<\/code> function will return the string passed in. With this approach, you can add
gettext<\/code> calls whenever you start thinking about localizing.<\/p>\n
Adding new translations in our template<\/h3>\n
gettext<\/code> in the templates, we need to update our POT and PO files.<\/p>\n
mix gettext.extract\nmix gettext.merge priv\/gettext\n<\/code><\/pre>\n
msgid<\/code> will be available in your PO files. You could also run both tasks with a single one:<\/p>\n
mix gettext.extract --merge\n<\/code><\/pre>\n
Adding new languages for our app<\/h3>\n
pt_BR<\/code>. The next step is running:<\/p>\n
mix gettext.merge priv\/gettext --locale pt_BR\n<\/code><\/pre>\n
.PO<\/code> files. Let’s see our structure after we ran the task:<\/p>\n
priv\/gettext\n\u2514\u2500 en_US\n| \u2514\u2500 LC_MESSAGES\n| \u2514\u2500 errors.po\n| \u2514\u2500 default.po\n\u2514\u2500 pt_BR\n| \u2514\u2500 LC_MESSAGES\n| \u2514\u2500 errors.po\n| \u2514\u2500 default.po\n\u2514\u2500 default.pot\n\u2514\u2500 errors.pot\n<\/code><\/pre>\n
pt_BR<\/code> and I didn’t translate the
msgid<\/code>? The Gettext will fall back to the default language and your app will not crash.<\/p>\n
You can do more with
Gettext<\/code>.<\/h2>\n
Pluralize<\/h3>\n
gettext \"Here is one string to translate\"<\/code>, you can pluralize. Example:<\/p>\n
# Plural translation\nnumber_of_apples = 4\nngettext \"The apple is ripe\",\n\"The apples are ripe\",\nnumber_of_apples\n<\/code><\/pre>\n
msgid \"The apple is ripe\"\nmsgid_plural \"The apples are ripe\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n<\/code><\/pre>\n
Interpolation<\/h3>\n
msgid<\/code>s or
msgid_plural<\/code>s by enclosing them in
%{<\/code> and
}<\/code>, like this: Example:
gettext(\"My name is %{name}\", name: @user.name)<\/code>. The PO file will be like:<\/p>\n
msgid \"My name is %{name}\"\nmsgstr \"\"\n<\/code><\/pre>\n
Domains<\/h3>\n
errors<\/code> for validations. To use a custom domain, use:
dgettext \"errors\", \"Here is an error message to translate\"<\/code>. The tasks will be responsible for creating the files for this domain.<\/p>\n
Locale per-process<\/h3>\n
Gettext.put_locale\/2<\/code> in a new process in order to change the locale for that process. You can change the default value in your
config\/config.exs<\/code> with
config :playfair, Playfair.Gettext, default_locale: \"pt_BR\"<\/code>.<\/p>\n
gettext<\/code> functions, you probably need to work in some assets, currency, dates, and so on. But, the hard work, scanning all your files to add the translation, has already been done.<\/p>\n
Gettext<\/code> approach could have helped you?<\/p>\n
\n
\n<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"