Lokalise graphic on translating ruby apps

I18n gems: Translate your Ruby apps

Ruby is probably my most favorite programming language: it enables us to write expressive, flexible, and powerful code, it has a great community, and it’s rapidly evolving. In this article I would like to tackle a problem of translating Ruby applications and introduce multiple i18n gems that can help you solve this task. Specifically, we are going to discuss i18n gem, i18n-rails, r18n, fast_gettext, and mobility.

So, shall we start?

The source code for this article can be found on GitHub. For the purposes of this demo I’ll use Ruby 3 but all the code samples should work with Ruby 2 as well.

I18n gem

Overview

I18n gem is a de-facto standard for Ruby and currently it is maintained by Ryan Bigg, a great developer and an acquaintance of mine. It is a battle-tested solution used in numerous applications: from simplest to very complex. This solution has all the features you might need:

  • Basic translation and localization (by default translations are stored in YAML files).
  • Interpolation.
  • Pluralization.
  • Flexible default values.
  • Custom exception handlers.
  • Swappable backend (you can use ActiveRecord and other backends).
  • Additional pluggable features: cache, Gettext support, and more.

Setting up I18n gem

Getting started with the i18n gem is very simple. Drop the following line into your Gemfile:

gem 'i18n', '~> 1.9'

And run:

bundle i

Then you’ll have to decide where to store translation files. For the purposes of this demo, let’s create a locales directory with two files: en.yml and ru.yml (of course, you can choose any other languages).

Next, add the following code somewhere in your application. Please note that this code should be executed before performing any translations:

require 'i18n'

I18n.load_path << Dir[File.expand_path("locales") + "/*.yml"]
I18n.default_locale = :en

Please note that the second line is optional because the default locale is English (:en) but you can set it to any other language.

For simplicity, I’ll place the above code in a file called demo.rb.

Next, let’s see how to perform simple translations.

Performing simple translations

Let’s provide a simple translation inside the en.yml file:

en:
  welcome: Welcome to our demo app!

Please note that the en root key is mandatory. welcome is the translation key, whereas Welcome to our demo app! is the corresponding translation value.

Now, populate the ru.yml file:

ru:
  welcome: Добро пожаловать в демонстрационное приложение!

Next, place the following code inside the demo.rb file:

puts I18n.t(:welcome)

t is an alias for the translate method. It accepts a key name and returns the corresponding translation for the currently chosen locale. Nice!

Locale switching

Currently we always use the English locale. Let’s allow our users to choose the preferred language by passing a command-line argument in the following way: ruby demo.rb en or ruby demo.rb ru.

Let’s add the following line of code:

require 'i18n'
I18n.load_path << Dir["#{File.expand_path('locales')}/*.yml"]

I18n.default_locale = :en

requested_locale = ARGV[0]&.to_sym # <=====

Please note that you won’t be able to switch to an unavailable locale. The I18n module automatically detects all the available locales when you’re appending YAML files to the load_path. Therefore, let’s add a simple check:

requested_locale = ARGV[0]&.to_sym

unless I18n.available_locales.include?(requested_locale) || requested_locale.nil?
  puts 'We do not support this locale yet :( However, we do support:'
  puts I18n.available_locales.join("\n")
  exit
end

I18n.locale = requested_locale if requested_locale

So, if the requested locale is not supported yet, we display a list of available locales and exit from the program. Otherwise, set the locale using the locale= method.

Working with date and time

The I18n module provides a method called l (localize) that works with date and time. Use it in the following way:

puts I18n.l(Time.now, format: :short)

The first argument is the date-time object, whereas the second one is a hash with options. Specifically, you’ll need to choose the formatting style: in the example above it is set to :short.

Let’s define the :short formatting style inside en.yml file:

en:
  time:
    formats:
      short: "%Y/%b/%d %H:%M"

You can use the same directives as the ones listed for the strftime method. There’s a problem, however: if you’d like to print month names, those have to be translated separately (same applies to day names):

en:
  date:
    abbr_month_names:
    - 
    - Jan
    - Feb
    - Mar
    - Apr
    - May
    - Jun
    - Jul
    - Aug
    - Sep
    - Oct
    - Nov
    - Dec

You can grab this data from rails-i18n source which provides common translations for many languages.

Now let’s tweak the ru.yml file:

ru:  
  time:
    formats:
      short: "%d %b %Y %H:%M"
  date:
    abbr_month_names:
    - 
    - янв.
    - февр.
    - марта
    - апр.
    - мая
    - июня
    - июля
    - авг.
    - сент.
    - окт.
    - нояб.
    - дек.

That’s it, now our date and time is properly localized!

Pluralization and interpolation

Pluralization is another common task and the I18n gem helps us to solve it as well. Let’s suppose we would like to display how many incoming messages the user has and who sent it:

puts I18n.t(:incoming_messages, count: 11, sender: 'Ann')

Depending on the count, we would like to slightly adjust the text. The I18n gem already contains pluralization rules for English locale, but unfortunately it does not provide rules for other languages out of the box. The good news is that you can find these rules in the following file. To learn more about pluralization logic in different languages, check the Unicode CLDR docs.

For example, to provide Russian pluralization rules, create a new file ru.rb inside the locales/pluralization folder and paste the following code:

{
  :ru => { :i18n => { :plural => { :keys => [:one, :few, :many, :other], :rule => lambda { |n| n % 10 == 1 && n % 100 != 11 ? :one : [2, 3, 4].include?(n % 10) && ![12, 13, 14].include?(n % 100) ? :few : n % 10 == 0 || [5, 6, 7, 8, 9].include?(n % 10) || [11, 12, 13, 14].include?(n % 100) ? :many : :other } } } },
}

We also have to add this file to the load_path:

I18n.load_path << Dir["#{File.expand_path('locales')}/*.yml"]
I18n.load_path << Dir["#{File.expand_path('locales')}/pluralization/*.rb"] # <===

Once you are done, open the en.yml file and provide English translations:

en:  
  incoming_messages:
    one: You have 1 incoming message from %{sender}
    other: You have %{count} incoming messages from %{sender}

English pluralization rules are quite simple and you have only two cases: there’s one message or there are many messages. Also note the %{count} and %{sender} placeholders — this is how you interpolate values into your translations.

Next, the ru.yml file:

ru:
  incoming_messages:
    one: У вас одно новое сообщение от %{sender}
    few: У вас %{count} новых сообщения от %{sender}
    many: У вас %{count} новых сообщений от %{sender}
    other: У вас %{count} новое сообщение от %{sender}

In this case we have to be a bit more eloquent but all in all the task is completed!

Working with gender information

Unfortunately, the I18n gem does not have any dedicated support for gender information but we can take advantage of translations nesting and introduce it ourselves.

First, let’s tweak the en.yml file:

en:
  says:
    male: He says %{content}
    female: She says %{content}
    neutral: Says %{content}

As you can see, we can provide as many options as needed: in the example above we’ve also added a gender-neutral phrase that can be used when, for example, you don’t know the user’s gender.

Let’s do the same for the ru.yml file:

ru:
  says:
    male: Он говорит %{content}
    female: Она говорит %{content}
    neutral: Говорит %{content}

And then let’s suppose the gender is stored in the gender variable:

gender = "female"
puts I18n.t("says.#{gender}", content: 'Hi there!')

As you can see, we simply interpolate the variable’s content inside the translation key name. This dot (.) represents a single level of nesting as our keys are nested under the says parent. Great job!

Use Lokalise to manage your translations

Honestly, managing multiple files and performing translations (especially for the language that you are not familiar with) is a complex task. Therefore Lokalise is here to save the day: we provide a convenient Web interface to manage your translations, collaborate with other team members, request professional translation services, and many more! And, what’s even cooler, there’s a dedicated integration for Ruby called lokalise_manager.

So, to get started:

  • Grab your 14-days free trial.
  • Log in to the system. You’ll see the project dashboard.
  • Click New project, enter your project name, and choose the base language: that’s the primary language of your application. Additionally choose one or more target languages: those are the languages you are going to translate into.
  • Proceed to the newly created project, and click More > Settings. Under the General tab, you’ll see a project id: copy this identifier as we’ll need it later.
  • Click on your avatar in the bottom left corner, and choose Profile settings.
  • Proceed to the API tokens and generate a new read-write token. We are going to use this token to upload and download translation files. Keep this token secure and never expose it!

Next, hook up a new gem:

gem 'lokalise_manager', '~> 2'

Then you can create a simple script to upload translation files from the app to your Lokalise project. Use the following code:

require 'lokalise_manager'
exporter = LokaliseManager.exporter api_token: 'YOUR_API_TOKEN', project_id: 'YOUR_PROJECT_ID'
expoter.export!

Provide your API token and the project ID. By default, the importer will fetch all YAML files from the locales directory, but this behavior can be further adjusted with a number of configuration options.

To download your files from Lokalise back to the Ruby app:

require 'lokalise_manager'
importer = LokaliseManager.importer api_token: 'YOUR_TOKEN', project_id: 'YOUR_PROJECT_ID'
importer.import!

The export task can be customized as well. As you see, the overall process is very simple and yet it’s much more convenient to manage your translations with Lokalise rather than perform editing and verification using the text editor. This is especially useful when you would like to hire professional translators that are usually not tech-savvy people and may have issues with the YAML format.

Rails-i18n gem

If you are using Ruby on Rails, you can take advantage of the rails-i18n gem which is based on the i18n library that we’ve just seen above. The rails-i18n gem contains some Rails-specific logic and also provides common translations for many languages out of the box: month and day names, time formats, etc. If you would like to learn more about this gem and see it in action, you can check my tutorial “Rails internationalization: Step-by-step” that covers all the necessary information.

Please note that Lokalise also provides a dedicated Rails integration that is based on the LokaliseManager gem described above. To take advantage of it, simply add a new gem into your Gemfile:

gem 'lokalise_rails', '~> 4'

Run the install command:

bundle
rails g lokalise_rails:install

Open the config/lokalise_rails.rb file and paste your API token and the project ID inside.

Finally, run the following Rake task to upload translation files from config/locales folder to Lokalise (of course, this behavior can be further adjusted):

rails lokalise_rails:export

And to download files from Lokalise to your Rails app run:

rails lokalise_rails:import

Very nice!

R18n gem

Overview

R18n is a gem that allows translating Ruby applications: it provides wrapper plugins for desktop apps, Rails, and Sinatra. While this solution is less popular then the I18n gem it’s still quite powerful and you might even prefer it over the de facto standard. In this tutorial I’ll show you how to work with the r18n-desktop solution.

This gem boasts the following features:

  • Ruby-style syntax.
  • Filters.
  • Model translation (or any Ruby object).
  • Auto-detect user locales.
  • Flexible locales.

Setting up r18n-desktop gem

First things first: let’s add the gem into our Gemfile.

gem 'r18n-desktop', '~> 5.0'

Install it by running:

bundle

Next let’s create a demo.rb file with the following code:

require 'r18n-desktop'
R18n.from_env('locales/')
include R18n::Helpers

from_env is going to load translation files from the locales directory and properly set up the available locales. R18n::Helpers is a module with some helper methods that will make our code look a bit more pretty.

Simple translations

Next, create the locales directory and place the en.yml file inside:

welcome: Welcome to our demo app!

R18n employs YAML files just like I18n does but there are some specifics: for one thing, there’s no need to define a root key representing the language code. There are some other peculiarities as we’ll see next.

Also let’s create the ru.yml file:

welcome: Добро пожаловать в демонстрационное приложение!

In order to translate something, you can use the t method (as we were doing with the I18n gem). The returned object responds to methods that are named after your translation keys. In other words, you can say:

puts t.welcome

Nice and clean!

Switching locale

Next, let’s see how to perform locale switching. As long as R18n automatically loads all the available locales, we can always check whether the requested language is supported or not:

requested_locale = ARGV[0] || 'en'

unless r18n.available_locales.map(&:code).include?(requested_locale)
  puts 'We do not support this locale yet :( However, we do support:'
  r18n.available_locales.each do |locale|
    puts "#{locale.title} (#{locale.code})"
  end
  exit
end

R18n.set(requested_locale)

First, we fetch the locale from the command-line arguments.

Next, check whether the locale (represented as a language code) is found in the available_locales array. As long as this method returns an array of R18n objects, we have to call code on each element.

If the locale is not available, we display a list of supported languages and their codes, and then exit from the program.

Otherwise, we simply set the chosen locale. Great!

Performing localization

Now, how do you localize date and time with the R18n gem? It’s nothing complex really:

puts l(Time.now, :standard)

In contrast to the I18n gem, R18n does provide common translations and pluralization rules out of the box therefore we don’t even need to define formats or provide month names ourselves.

Out of the box R18n provides the following formats: :standard (displays date, hours, and minutes), :full (displays full information), and :human (displays messages like “3 days ago”). Of course, you can define additional formats: check the source code to see an example.

Performing pluralization

Pluralization is natively supported by R18n and you don’t have to write pluralization rules yourself. Let’s add the following code:

puts t.incoming_messages(11)

So, depending on the argument, we would like to adjust the message.

Add translations to the  en.yml file:

incoming_messages: !!pl
  1: You have 1 incoming message
  n: You have %1 incoming messages

Note the !!pl part: it’s a filter and it means “pluralize”. %1 is a placeholder which means “insert the number of messages at this position”.

Next, let’s tweak the ru.yml file:

incoming_messages: !!pl
  1: У вас одно новое сообщение
  2: У вас %1 новых сообщения
  n: У вас %1 новых сообщений

That’s it!

Working with gender information

While R18n does not have native support for genders, this solution is customizable and you can create additional custom filters.

First, let’s see how our translations will look. Tweak the en.yml file:

says: !!gender
  neutral: The message is %2
  f: She says %2
  m: He says %2

!!gender is a custom filter that we’ll define in a moment. Note that you can add as many genders as you like. %2 is a placeholder which means “insert the message contents here”. Please note that I’m saying %2 because %1 (the first placeholder) is going to contain the currently set gender: of course, you can display it as well.

Now, edit the ru.yml file:

incoming_messages: !!pl
  1: У вас одно новое сообщение
  2: У вас %1 новых сообщения
  n: У вас %1 новых сообщений

Once you are ready, let’s create a new filter called gender:

R18n::Filters.add('gender', :gender) do |content, config, choice|
  content.fetch(choice) { content['neutral'] }
end
  • content is the actual translation value.
  • config represents R18n configuration.
  • choice is an argument that we are going to pass to the translation method. Potential values are "neutral", "f", or "m". However, the logic can be complex: for example, your argument might be represented as an object which responds to the gender method, and then this object is utilized to choose the proper translation.

Please note that the above code must be placed before translation files are loaded: in other words, put it before the from_env call.

Now let’s see how to use our new filter:

puts t.says('f', 'Hi there!')

The 'f' string will be assigned to the choice variable inside the block. We use the choice to pick one of the translation values. The second argument ('Hi there') will also be passed to the translation and you can reference it using the %2 placeholder.

Brilliant job!

FastGettext gem: Translate with PO files

Overview

Another solution that I would like to present today is called FastGettext. It’s a battle-tested solution which has been evolving for years, and it’s employed by numerous applications.

It has the following features:

  • Ability to work with various file types including PO, MO, and YAML.
  • Ability to use other backends like ActiveRecord to store translations.
  • Very fast.
  • Thread-safe.
  • Extendable.

Setting up

So, tweak your Gemfile:

gem 'fast_gettext', '~> 2.2'

Install the newly added gem:

bundle

Next, let’s paste the following code to the demo.rb file:

require 'fast_gettext'

extend FastGettext::Translation

Now we’re ready to get rolling!

Adding translations

Now let’s create the locales directory with en and ru folders. Inside each folder create files called demo.po.

en/demo.po:

# DEMO TRANSLATIONS.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

msgid "Welcome to our demo app!"
msgstr ""

Note the Plural-forms part: it defines pluralization rules for the current locale, and we’re going to use it a bit later.

ru/demo.po:

# DEMO TRANSLATIONS.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"

msgid "Welcome to our demo app!"
msgstr "Добро пожаловать в демонстрационное приложение!"

Now we have to load these translations, set the domain name, and adjust the supported locales. Therefore, adjust the demo.rb file:

FastGettext.add_text_domain('demo', path: 'locales', type: :po)

FastGettext.available_locales = %w(en ru)

FastGettext.text_domain = 'demo'

Very nice!

Switching locale

Next, let’s also enable our users to perform locale switching. The overall logic will be similar to that we did with I18n and R18n gems:

requested_locale = ARGV[0] || 'en'
unless FastGettext.available_locales.include?(requested_locale) || requested_locale.nil?
  puts 'We do not support this locale yet :( However, we do support:'
  puts FastGettext.available_locales.join("\n")
  exit
end

FastGettext.locale = requested_locale

As you can see, nothing too complex: we just check the list of all available locales and either exit the program or set the requested language.

Simple translations

Now, how do you perform simple translations with FastGettext? Use the following code sample:

puts _('Welcome to our demo app!')

_ is a method to perform the actual translation. It will search the requested msgid and fetch the corresponding msgstr (as long as our message IDs are in English, we don’t need to provide msgstr for this locale).

Performing pluralization

Next, let’s see how to use pluralization with Gettext. We have to be a bit more eloquent:

puts n_('incoming message', 'incoming messages', 10)  % { amount: 10, sender: 'Ann'}

% is a Ruby method that we use to interpolate the necessary values into the translation. As long as we have already specified pluralization rules, you’ll just need to provide the translations.

en/demo.po:

msgid "incoming message"
msgid_plural "incoming messages"
msgstr[0] "%{amount} incoming message from %{sender}"
msgstr[1] "%{amount} incoming messages from %{sender}"

ru/demo.po:

msgid "incoming message"
msgid_plural "incoming messages"
msgstr[0] "%{amount} входящее сообщение от %{sender}"
msgstr[1] "%{amount} входящих сообщения от %{sender}"
msgstr[2] "%{amount} входящих сообщений от %{sender}"

Working with gender information

The last thing I’m going to show you is how to work with gender information. It’s quite simple as well:

puts s_('female|says') % { content: "Hey there!" }

The female| part will act as a namespace, and to properly handle it we have to use the s_ method. You can provide any other gender as a namespace.

Next, provide translations inside the en/demo.po file:

msgid "male|says"
msgstr "He says: %{content}"
msgid "female|says"
msgstr "She says: %{content}"
msgid "neutral|says"
msgstr "Says: %{content}"

And, finally, ru/demo.po:

msgid "male|says"
msgstr "Он говорит: %{content}"
msgid "female|says"
msgstr "Она говорит: %{content}"
msgid "neutral|says"
msgstr "Говорит: %{content}"

That’s it!

Gettext files and Lokalise

Please note that Lokalise also has full support for PO and POT files, and you can easily upload them using our web interface, via command line interface, or via the API.

Mobility gem

So, we have covered multiple i18n gems but in certain cases you might want to store your translations inside the database rather than in YAML or PO files. In this case the Mobility gem is what you’re looking for: it is powerful yet easy to use. To learn more about this solution, please check my tutorial “Storing translations inside the database with Mobility” that covers all the necessary information.

Conclusion

So, in this article we have covered multiple i18n gems that can be used to translate your Ruby applications. Specifically, we have seen I18n gem, RailsI18n, R18n, FastGettext, and Mobility. As you can see, there’s a lot to choose from!

And that’s it for today, folks. Thank you for staying with me, and happy coding!

Related posts

Learn something new every week

Get the latest in localization delivered straight to your inbox.

Related articles
Localization made easy. Why wait?
The preferred localization tool of 2000+ companies