This concludes the Ruby month of my tutorial series. So before I look at the interesting case of the ruby gettext approach to internationalization (using the fast_gettext gem) here are the links to previous articles on internationalization for Ruby that I have covered so far (in case you want to catch up):

ruby1-150x150

Ruby gettext internationalization: What Fast Gettext can do

The topic of our fourth and final article will be covering the fast_gettext gem. Fast Gettext features simplicity of use, clean namespace, it is thread safe, extendible and it supports multiple backends. Since translations are cached after the first use, performance is almost the same for all the backends.

When it comes to the performance, it is claimed on the fast_gettex Github page that it  is much faster and much more efficient then the competition:

HashFastGettextGetTextActiveSupport I18n::Simple
Speed\*0.82s1.36s4.88s21.77s
RAM\*4K8K4480K10100K
Included backendsdb, yml, mo, po, logger, chainmoyml (db/key-value/po/chain in other I18n backends)
\*50.000 translations with ruby enterprise 1.8.6 through rake benchmark

We already wrote extensively about Gettext in some of our previous articles, you can get back to them in order to read more about PHP internationalization with Gettext or setting up gettext backend for i18n Ruby gem.

Setting up for ruby gettext: Installation of fast_gettext

$ sudo gem install fast_gettext

Setting the text domain and current locale

This should be done once in every Thread (e.g. Rails -> ApplicationController):

FastGettext.text_domain = 'my_app'

# only allow these locales to be set (optional)

FastGettext.available_locales = \['de','en','fr','en_US','en_UK'\]
FastGettext.locale = 'de' 

Translation lookup with fast gettext

include FastGettext::Translation
*('Car') == 'Auto'
\('not-found') == 'not-found'
s*('Namespace|no-found') == 'not-found'
n('Axis','Axis',3) == 'Achsen' #German plural of Axis
_('Hello %{name}!') % {:name => "Pete"} == 'Hello Pete!'

Managing translations - MO/PO files

PO files are plain text files and can be created in any plain text editor. There is a command line tool from the GNU gettext package that can automatize this process to a degree. After the PO file is ready, it can be converted to an MO file using a command line tool from the same package. We have covered this process in detail in one of our previous articles. The author of fast_gettext has started to work on MO/PO parser/generator, so you can check that project as well.

There are also rake tasks that can be used or customized.

When PO or MO files are ready, it is possible to load either of them:

MO files:

FastGettext.add_text_domain('my_app',:path => 'locale')

PO files:

FastGettext.add_text_domain('my_app',:path => 'locale', :type => :po)

# :ignore_fuzzy => true to not use fuzzy translations

# :report_warning => false to hide warnings about obsolete/fuzzy translations

Managing translations - database

Any model DataMapper/Sequel or any other (non-database) backend can be used. The only thing that is required is to implement the response to self.translation(key, locale) call. The plurals are separated by default with pipes ( | ), but it is possible to overwrite this. Database access is cached, so only the first lookup hits the database.

require "fast_gettext/translation_repository/db"
FastGettext::TranslationRepository::Db.require_models #load and include default models
FastGettext.add_text_domain('my_app', :type => :db, :model => TranslationKey)

Managing translations - yaml

If you already have a bunch of yaml files, or you just like the format and yet you want to exploit the performance of fast gettext, no problem, it supports yaml. The syntax and indentation rules should be used as in i18n.

FastGettext.add_text_domain('my_app', :path => 'config/locales', :type => :yaml)

Rails plugin

There is a rails plugin that simplifies the setup.

To install it as a gem, just include it in Gemfile and run bundle:

gem 'gettext_i18n_rails'

The plugin can make use of the i18n Rails localization, you just need to add the files to config/locales directory.

To initialize:

# config/initializers/fast_gettext.rb
FastGettext.add_text_domain 'app', :path => 'locale', :type => :po
FastGettext.default_available_locales = \['en','de'\] #all you want to allow
FastGettext.default_text_domain = 'app'

And in your application:

# app/controllers/application_controller.rb
class ApplicationController < ...
before_filter :set_gettext_locale

Any call to I18n that matches a gettext key will be translated through FastGettext:

I18n.locale <==> FastGettext.locale.to_sym
I18n.locale = :de <==> FastGettext.locale = 'de'

Pluralization

Plurals are selected by index, for example: ['car', 'cars'][index], and the pluralisation rule decides which form will be used. By default, only the plural form for English and other similar languages is supported:  count == 1 ? 0 : 1

If a language that uses a different plural form needs to be supported, a custom pluralisation rule needs to be added either via Ruby:

FastGettext.pluralisation_rule = lamda{|count| count > 5 ? 1 : (count > 2 ? 0 : 2)}

or via mo/po file:

Plural-Forms: nplurals=2; plural=n==2?3:4;

Defaults

If only one text domain is used, setting FastGettext.default_text_domain = 'app' is sufficient and no more text_domain= is needed.

If the simple rule of "first availble_locale or 'en'" is not suficcient, FastGettext.default_locale = 'de' can be set.

If no available_locales are set, fallback can be set in default_available_locales.

Loading multiple repositories

Any number of repositories can be used to find a translation, and when the first can not translate a given key, the next one is searched:

repos = \[
FastGettext::TranslationRepository.build('new', :path=>'....'),
FastGettext::TranslationRepository.build('old', :path=>'....')
\]
FastGettext.add_text_domain 'combined', :type=>:chain, :chain=>repos

Logging

It can be useful to keep track of the keys that could not be translated or that were used. In order to do so, a Logger should be added to a Chain:

repos = \[
FastGettext::TranslationRepository.build('app', :path=>'....')
FastGettext::TranslationRepository.build('logger', :type=>:logger, :callback=>lamda{|key_or_array_of_ids| ... }),
}
FastGettext.add_text_domain 'combined', :type=>:chain, :chain=>repos

If the Logger is the first in the chain, it will record all the used translations. However if it is last, it will record only those that were not found.

Some other useful Ruby gettext packages

Ok, so far we have covered i18n gettext backend (in one of the previous articles) and fast gettext. It does not end there, there are some other solutions as well:

  • Gettext for Ruby - an original package; fast_gettext was created as a response to it; this project has not been updated for some time now
  • GettextI18nRailsJs - an extension of gettext_i18n_rails that makes the PO files available client side via javascript (as JSON)

Conclusion

This brings us to the end of our internationalization for Ruby series. To sum up the various alternatives:

  • Ruby i18n - comes as a Rails default and would be natural first choice for the Rails developers. Can be easily used in plain Ruby or other frameworks as well. Supports multiple backends and resource files. It is quite easy to use.
  • r18n - an alternative to i18n, can be used as a superset to it, has somewhat different approach to translation lookup and also add some interesting features to yaml files (filters).
  • Ruby gettext - has not been maintained for a while. Several other packages have risen as a response to it.
  • Ruby gettext using fast_gettext - very light and yet powerful. Also supports backends and resource files other then gettext, with no or not much additional cost in performance.

Further reading

Try lingohub 14 days for free. No credit card. No catch. Cancel anytime