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.
By leveraging software internationalization, we can ensure that our Ruby applications are not only functional but also accessible and engaging for users from diverse linguistic backgrounds.
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.
By incorporating the I18n gem into your applications, you can enhance the overall localization process, making it easier to manage and adapt your content for a global audience.
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 translation and localization 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 thegender
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!