Software localization
Localizing JavaScript in Rails Apps
For the past months we have published a couple of articles covering I18n in Rails but they were mainly focusing on back-end topics. What should you do, however, in order to use Rails translations in the JavaScript code as well? It appears that there is already a solution available solving this problem called i18n-js. It works with Rails and other technologies like Python or PHP. The basic idea behind this library is simple: it allows you to utilize translations from the config/locales right in your JavaScript code in the same way it is done in Rails. So, this library has t
and l
methods that behave pretty much the same as their counterparts in Rails I18n module. In this article you will learn how to integrate the i18n-js library into your application and how to work with it. The source code for this is available here.
🔗 Resource » Check out our Ultimate Guide on JavaScript Localization for all the steps you need to make your app accessible to users across the world.
Preparations
For demonstration purposes we'll create a new Rails application, but you, of course, may employ some existing app instead:
$ rails new I18nJS
Rails 5 will be used for this demo, but the i18n-js library works with earlier versions as well (and also supports applications without the Asset Pipeline). Next, edit your Gemfile: Gemfile
# ...
gem "i18n-js"
Don't forget to install the gem:
$ bundle install
After that you will need to include the required JavaScript libraries as instructed by the docs: javascripts/application.js
//= require i18n
//= require i18n/translations
That's pretty much it - you can start working with the library now. Before we proceed to the main part, however, let's allow users to switch locales.
Switching Locales
First, define all available locales in the global configuration. We'll add support for English and Russian, but you may want to choose any other languages: config/application.rb
config.i18n.available_locales = [:ru, :en]
Now we'd like to provide the currently chosen locale on the page. The most obvious place for that is the lang
attribute inside the html
tag, but unfortunately if you are using Turbolinks it won't work properly. The thing is Turbolinks do not touch the html
tag when the page is reloaded, so if a user clicks a link to switch the language, the lang
attribute will be the same as upon the initial page load. Therefore, I'll employ the body
tag instead: layouts/application.html.erb
<body data-locale="<%= I18n.locale %>">
Now let's provide a way to switch the locale. I'll stick with the easiest way and simply utilize the locale
GET param. There are a bunch of other ways to solve this task and you may read about them in our article Setting and Managing Locales in Rails i18n. Tweak your ApplicationController
to include a new before_action
: application_controller.rb
# ... before_action :set_locale private def set_locale I18n.locale = extract_locale || I18n.default_locale end def extract_locale parsed_locale = params[:locale] I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end
So, on each request we try to set a locale. If it was found inside the locale
GET param and the chosen language is supported, we go ahead and set it. Otherwise, use a default locale which is English. Also, let's persist the chosen locale between requests by globally tweaking the URL options: application_controller.rb
# ... def default_url_options { locale: I18n.locale } end
This method will add the locale
option for all links generated with Rails route helpers. Lastly, present the links to actually switch the language: layouts/application.html.erb
<ul> <li><%= link_to 'English', root_path(locale: :en) %></li> <li><%= link_to 'Русский', root_path(locale: :ru) %></li> </ul>
So far so good, but the locale for the i18n-js library won't be changed automatically unless we explicitly say to do so. Therefore, create a new CoffeeScript file and perform this operation upon page load. I am listening for a special Turbolinks event, but if you are not using this library, replace it with just $ ->
: javascripts/localization.coffee
$(document).on 'turbolinks:load', -> I18n.locale = $('body').data('locale')
Don't forget to require this file: javascripts/application.js
// ...
//= require i18n //= require i18n/translations //= require localization //= require turbolinks This is it, now the language will be properly changed on both server-side and client-side!
Sample Pages
Of course we will need a couple of sample pages, so create two routes now: config/routes.rb
# ... get '/about', to: 'pages#about' root 'pages#index'
Define a new controller: pages_controller.rb
class PagesController < ApplicationController end
The actions will be empty, so we don't really need to create them inside. However, do create two empty views named index.html.erb and about.html.erb inside the views/pages directory. Add the links to these new pages:
<ul> <li><%= link_to 'Home', root_path %></li> <li><%= link_to 'About', about_path %></li> </ul>
layouts/application.html.erb Okay, now boot the server, visit the root page and open the browser's console. To see which locale is currently set, run the following code:
I18n.currentLocale()
You should see a currently chosen locale, which means that everything is working fine!
I18n-JS in Action
Now let's perform some translations. I'll write JavaScript right inside the views, but you may extract the code to the CoffeeScript files. For example, let's localize the home page title: views/pages/index.html.erb
<h1 class="page-title"></h1> <script> $(document).on('turbolinks:load', function() { $('.page-title').text(I18n.t('pages.index.title')); }); </script>
As you see, the t
method to actually perform translation is the same as the one used in Rails. Provide translations: config/locales/en.yml
en: pages: index: title: 'Hello!'
config/locales/ru.yml
ru: pages: index: title: 'Привет!'
The scope can be passed as an option as well: views/pages/index.html.erb
$('.page-title').text(I18n.t('title', {scope: 'pages.index'}));
So if you are using the same scope in multiple places, this option can be stored in a variable.
Fallbacks
What happens if a translation cannot be found for some reason? Well, by default a message like [missing "ru.pages.index.title" translation]
will be displayed. However, you can enable fallbacks by setting the fallbacks
option to true
:
I18n.fallbacks = true
The library will firstly search for a translation in less specific versions of a locale, for example ru-RU
and then just ru
. Then it will try to use translation from the English locale. To override the fallback rules, define them for some locale like this:
I18n.locales.ru = ["de", "en"];
This setting also accepts a string or a function that returns some value. What's more, you can set missingBehaviour
option:
I18n.missingBehaviour = 'guess';
If the translation cannot be found, the key itself will be titleized and used as a translation, just like Rails does. So, for instance, pages.title.hello
will return Hello
. As with Rails I18n module, you can set the default values for the missing translations as well. The I18n.t
method accepts an object as a second optional argument:
I18n.t("some.missing.scope", {defaultValue: "A default message"});
Interpolation
Okay, now what about interpolation? It it easy to add as well. For example, let's greet a user and display his name inside the title: config/locales/ru.yml
ru: pages: index: title: 'Привет, %{name}!'
config/locales/en.yml
en: pages: index: title: 'Hello, %{name}!'
Now simply provide the value for the name
placeholder inside an object:
$('.page-title').text(I18n.t('pages.index.title', {name: 'Joe'}));
Simple, isn't it? Pluralization also works in the usual way. Let me, for example, display how many new messages the user has: views/pages/index.html.erb
<p class="messages-count"></p> <!-- ... --> <script> $(document).on('turbolinks:load', function() { $('.page-title').text(I18n.t('pages.index.title', {name: 'Joe'})); $('.messages-count').text(I18n.t('pages.index.messages', {count: 1})); }); </script>
Add translations: config/locales/en.yml
en: pages: index: title: 'Hello, %{name}!' messages: zero: 'You dont have new messages' one: 'You have one new message' other: 'You have %{count} new messages'
For Russian language pluralization rules are a bit more complex, so additional keys should be defined: config/locales/ru.yml
ru: pages: index: title: 'Привет, %{name}!' messages: zero: 'У вас нет новых сообщений' one: 'У вас одно новое сообщение' few: 'У вас %{count} новых сообщения' many: 'У вас %{count} новых сообщений' other: 'У вас %{count} новых сообщений'
Date and Number Formatting
Let's also load some common localization data using the rails-i18n gem. It provides pluralization, date, time and currency formatting rules and more. Gemfile
# ... gem 'rails-i18n'
Install the gem:
$ bundle install
I18n-js library also provides the l
method, that performs localization. Here are some examples of using it:
I18n.l("currency", 30.1); I18n.l("number", 10.5); I18n.l("percentage", 5.9);
On top of that, there are methods toNumber
, toCurrency
and toPercentage
that can be called upon the I18n
object. These methods accept options like precision
, separator
, delimiter
and strip_insignificant_zeros
so you have full control of how the result will look like. Localizing date and time is easy to perform as well. The library already provides a couple of formats that you can use right away. Simply specify the format's name and provide date or time as a second argument to the l
method: views/pages/index.html.erb
<p class="current-date"></p> <script> // ... $('.current-date').text(I18n.l("date.formats.long", new Date())); </script>
The l
method accepts not only a date object, but any string formatted as date/time or an Epoch time. To add new formats, tweak the translations
object for the chosen language:
I18n.translations["en"] = { date: { formats: {} } }
The library supports a handful of directives similar to the ones provided in Ruby. What's more, the library also has the strftime
method that you've probably used many times in Rails:
I18n.strftime(new Date(), "%d %m %Y");
Using Phrase to Manage Translations
Before wrapping up let me also explain how to integrate your Rails project with Phrase to manipulate translations easily, find missing keys and collaborate with professional translators. This is done in four simple steps:
- First of all, download the Phrase client and install it.
- Perform initialization by running
phraseapp init
command in the project's root. This command will perform the basic setup for the project. Choose YAML as the preferred format - Synchronize your local files with Phrase by running
phraseapp push
command. All your translations stored inside the config/locales directory will be uploaded to the Translation Center and you can start working with them right away. You can provide custom locations by tweaking theconfig.i18n.load_path
setting. - After you are done editing your translations, download them back by running
phraseapp pull
- As an optional step, you may add the phraseapp-ruby gem into the Gemfile to perform more complex API interactions
Conclusion
So, in this article we have covered i18n-js gem, allowing us to provide translations for JavaScript in Rails applications. This library is very convenient, yet simple to use, so we'd highly recommend giving it a try. Note, however, that this solution has a couple of known issues, so watch out for these. We thank you for staying with us and wish you happy coding!