Software localization

Localizing JavaScript in Rails Apps

Learn how to integrate the i18n-js library into your application and how to work with it.
Software localization blog category featured image | Phrase

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 the config.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!