Rails GetText Syntax Overview

In this article, we will give you a quick overview of how to translate your Rails application with the GetText syntax. There is only a few (short-named) methods to remember, we will make you love underscores!

TL;DR

Singular _("text")
Plural n_("singular text", "plural text", number)
Singular with context p_("context", "text")
Plural with context np_("context", "singular text", "plural text", number)

Simple Texts

_ is the base method you will use everywhere.

In a controller :

class PostsController < ActionController::Base
  def create
    ...
    flash[:notice] = _('Post successfuly saved')
    redirect_to posts_path
  end
end

In a view :

<h1><%= _('Create a New Post') %></h1>
<%= f.label :title, _('Title') %>

In a model validation :

class Post < ActiveRecord::Base
  validates :title, :presence => {
                      :message => lambda { _('Title is mandatory.') }
                    }
end

Note that the call was wrapped into a lambda because otherwhise it would have been statically evaluated.

Interpolations

Interpolations can be easily performed using the standard % operator. Find some examples below.

_('Hi %{name}, do you like violence?') % { name: "Steve" }
_('%{city1} is bigger than %{city2}') % { city1: "NYC", city2: "BXL" }

Plural Forms

If the translatable string is depending on the number of items, use _n. It takes three arguments : the singular form, the plural form, and the cardinality.

For example :

n_("There is a mouse in our house.", "There are some mice in our house.", @mice.size)

And you can of course combine with interpolations :

n_("There is a mouse.", "There are %{num} mice.", @mice.size) % { num: @mice.size }

Translation Contexts

Sometimes you would need to scope a given translation to a given part of you application. For instance in order to be able to translate a given string differently depending on the context.

There are two dedicated methods for that : p_ (singular with context) and np_ (plural with context). They take the same arguments as _ and n_ (respectively) with an extra string argument defining the context added before.

Here is an example :

p_("Menu", "Open file")
p_("Menu", "Close file")

With plurals and interpolations :

np_("Menu", "Open a file", "Open %{n} files", 4) % { n: 4 }

If you are wondering why it is p for defining a context, that is because it stands for prefix.

Other Syntaxes

Some other special usages of GetText can also be used but won't be detailed here because they are very specific (and not so useful) : s_, ns_, N_ and Nn_.

Dealing with YAML files and their Directory Structure

This article is about the way Translation.io deals with YAML files. Whether you prefer YAML over GetText or you use GetText but dealing with a few YAML keys is inevitable, this page is for you!

Since YAML doesn't guarantee that only a language is contained in a single file, we unfortunately are not able to mirror your source YAML structure.

Init & sync

Initializing your project implies :

  • All .yml files containing keys of your source language will be kept.
  • All .yml files containing keys of your target languages will be grouped into one named translation.LANGUAGE.yml.
  • All localization keys will be separated into localization-specific files. These files are named localization.LANGUAGE.yml.

Let's see this in details.

translation.xx.yml files contain all the "textual" YAML keys from your project. Once your translators translated them in Translation.io, you can sync and they will be updated. You cannot edit them directly: they will be overridden at each sync.

localization.xx.yml files contain all the non-textual (date formats, booleans, variables) keys that your translators will not understand. We populate them with the correct values for the target languages when initializing the project. These files are not synced with Translation.io. So you can update them manually if you need to. You can also configure other keys to be treated as localization.

Adding new keys

When a new key is needed:

  • Add it to one of your source files.
  • The next sync will send this new key to Translation.io and the key will be added to all target translation.xx.yml files.
  • After the key is translated in Translation.io, one more sync and your application translations will be up-to-date.

Example

Let's say your project is in en and is translated to fr and es. And you have these files at the moment :

  • config/locales/en.yml
  • config/locales/stuff.en.yml
  • config/locales/en/other.en.yml
  • config/locales/fr.yml
  • config/locales/stuff.fr.yml
  • config/locales/es.yml
  • config/locales/stuff.es.yml

After an init, you will have these files :

  • config/locales/en.yml
  • config/locales/stuff.en.yml
  • config/locales/en/other.en.yml
  • config/locales/translation.fr.yml
  • config/locales/translation.es.yml
  • config/locales/localization.fr.yml
  • config/locales/localization.es.yml

Same file struture for source language and two files per target language for localization.

Why GetText is Better than Rails I18n

We have a strong opinion about how an application in general, and more especially a Rails application, should be internationalized. A lot of people use the default Rails Internationalization API (I18n) but we don't really think this is an ideal solution for big projects.

Our reasons.

1. It's painful to "make up" keys. Need to switch between many files.

In a system like YAML, you have to invent the keys.

For example the welcome title of the main page of an app may be in t("home.title") but you can end up with very verbose things like t("on_call_schedules.contact_form.on_call_schedule_comment_placeholder").

2. Not trivial to search text in code.

When you want to change a text, you have to look for its key in the codebase, then find the text itself in the corresponding YAML file. Doesn't sound very practical to us.

With GetText, editing a text is just a matter of finding it and updating it, right in place.

3. Difficult coevolution between keys and associated text.

You've found a nice, short and descriptive key name for a given text. A few weeks later the customer asks for a significant change in the source language of that text. In such a situation you also need to change the key according to its new signification. Sometimes you won't, or you will forget. Then you will probably end up with a key name not representative of its content.

4. Difficult to know if all the keys are really used in the application.

All the translation keys are located at the same place : in YAML file(s). They are also organized as a tree. When the key is used in the code, it is formatted with a dot syntax like my.key. When you are in a YAML file, it's not easy to perform a "find in project" with your editor to see if it is still used or not. Because you have to convert the path in the tree to dot syntax before.

With the GetText paradigm, unused strings will be automatically detected at POT file generation. And with Translation.io you can safely remove all unused strings with the rake translation:sync_and_purge task.

5. YAML can contain values that are not translations.

Rails YAML files contain not only translation strings but also localization values (integers, arrays, booleans) in the same place and that's bad. For example : date formats, number separators, default currency or measure units, etc.

A translator is supposed to translate, not localize. That's not his role to choose how you want your dates or numbers to be displayed, right ? Moreover, this special keys often contain special constructions (e.g., with percent signs or spaces) that he might break.

We think localization is part of the configuration of the app and it should not reach the translator UI at all. That's why these localization keys are detected and separated on a dedicated YAML file with Translation.io.

6. GetText is a standard in the Open-Source community.

GNU GetText has been around since 1995 and is the de-facto translation solution for many projects. So why not using it in Rails applications ?

7. GetText syntax is nice.

GetText syntax looks optimal to us: short method names because used everywhere in the codebase and easy to remember. Ok t is a short name too ;-)

However, YAML has some advantages too.

1. Pre-translated YAML files for Gems.

That's true it's easier to ship the translatable strings of a Gem as a YAML file. No dependency at all because Rails ships with YAML.

2. Allows to copyright text in the source language.

With YAML, if you are using an external service like Locale, you can use keys to write a new text in the source language, not only in the target languages. It creates a lot of situations where you need to deal with conflicts, but this is not possible to do the same thing with GetText.

Also, as explained above, you have to be careful when you update the keys in order to keep them relevant.

When it's a key coming from Rails, you can override the source text it in your own YAML file too.

Conclusion

We, Translation.io, propose two ways for translating your applications, but we strongly encourage you to use GetText as your main workflow since your translation process will be way smoother.

Coming from FastGettext

If you are currently using FastGettext and/or gettexti18nrails to translate your Rails application and you would like to switch to Translation.io, this article will help you with the process of updating your codebase to make it work.

If you are not sure what you are doing, consider working in branch :-)

The Gemfile

The Gemfile should only contain the translation gem for all environments :

gem 'translation'

Make sure to remove any references to other gems like :

gem 'gettext_i18n_rails'
gem 'gettext',     :require => false
gem 'ruby_parser', :require => false
gem 'gettext'

You don't have to include gettext neither, translation already depends on its right version and will therefore include it for you.

Initializer

You probably have a fast_gettext.rb or a gettext.rb initializer file containing this kind of configuration :

FastGettext.add_text_domain 'app', :path => 'config/gettext_locales',
                                   :type => :po
FastGettext.default_available_locales = ['en', 'fr', 'de', 'nl']
FastGettext.default_text_domain       = 'app'

You won't of course need it anymore.

New Translation.io project

When you create a project on Translation.io, you will be asked to create an initializer named translation.rb :

TranslationIO.configure do |config|
  config.api_key        = 'xxxxxxxxxxxxxxxxxxxxxxxx'
  config.source_locale  = 'en'
  config.target_locales = ['fr', 'de', 'nl']

  # Uncomment this if you don't want to use gettext
  # config.disable_gettext = true

  # Uncomment this if you already use gettext or fast_gettext
  # config.locales_path = File.join('path', 'to', 'gettext_locale')
end

There you have to correclty set config.target_locales to the former target locales.

It is very likely that your existing POT/PO files are not in config/locales/gettext. In that case, just move them there (keeping the right structure) or set config.locales_path to the correct base location used by FastGettext (usually Rails.root.join('locale')).

Translation.io only supports one unique textdomain named app. If you were using multiple textdomains, you will have to merge them. If your domain was not named app you will have to rename all the PO/POT files to app.xx.

Controllers and before filter

You probably have a before_action somewhere in your controller hierarchy to set the current locale (I18n.locale) according to the URL, the session, a cookie, etc.

translation ships with a generic implementation that can be called directly from all ActionController::Base descendants :

before_action :set_locale

But you can of course continue to use your own method if you need custom behaviour by defining it. For instance :

before_action :set_locale

def set_locale
  I18n.locale = current_user.locale || I18n.default_locale
end

That's it

After performing this set of changes, you should be ready to translate your rails applications with Translation.io. Don't hesitate to contact us if you think something is missing or not clear enough.