Javascript internationalization in Ruby on Rails

Sooner or later you'll face with javascript internationalization if you are developing a multilingual application. There is a wonderful i18n-js gem in Ruby on Rails for that. In spite of existing manuals, it took some time for me to figure out how it works.

Let's localize select2 messages from the example in the previous article.

First, we should configure internationalization according to Rails manual.

Set options in the config/initializers/locale.rb config

    # config/initializers/locale.rb
    # dictionaries will be searched recursivly in config/locales
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
    config.i18n.default_locale = :en
    config.i18n.available_locales = [:ru, :en]

We will get the current locale from page url and will set it in ApplicationController

# app/controllers/application_controller.rb
  before_action :set_locale

  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end

Let's add locale to all generated links

# app/controllers/application_controller.rb
  def default_url_options(options={})
    { locale: I18n.locale }
  end

And finally, set up locale in routes

# config/routes.rb
  scope "/:locale", locale: /#{I18n.available_locales.join("|")}/ do
    resources :posts
    root to: redirect("/%{locale}/posts", status: 302)
  end

  root to: redirect("/#{I18n.default_locale}", status: 302), as: :redirected_root

  get "/*path", to: redirect("/#{I18n.default_locale}/%{path}", status: 302), constraints: {path: /(?!(#{I18n.available_locales.join("|")})\/).*/}, format: false

All the magic of redirects above is described in a separate blog post.

Let's create special helper to switch locales in layout

# app/helpers/application_helper.rb
module ApplicationHelper
  def lang_switcher
    content_tag(:ul, class: 'lang-switcher clearfix') do
      I18n.available_locales.each do |loc|
        locale_param = request.path == root_path ? root_path(locale: loc) : params.merge(locale: loc)
        concat content_tag(:li, (link_to loc, locale_param), class: (I18n.locale == loc ? "active" : ""))
      end
    end
  end
end
# app/views/layouts/application.html.erb
<%= lang_switcher %>

I usually use rails-i18n gem for some basic localization, but it's not required in this case. We should just add i18n-js gem to our Gemfile and install gems via bundler by running bundle install

Now we should add client scripts to provide javascript internationalization

# app/assets/javascripts/application.js
//= require i18n

We can create layout partial where server-side locale settings will be passed to client side javascript

# app/views/layouts/_js_locales_info.html.erb
<%= javascript_tag do %>
    I18n.defaultLocale = "<%= I18n.default_locale %>";
    I18n.locale = "<%= I18n.locale %>";
    I18n.fallbacks = true;
<% end %>

And then we just include this partial in our main layout's head section right after common js includes.

# app/views/layouts/application.html.erb
  <%= render 'layouts/js_locales_info' %>

I should note, that in this case we are able to use internationalization only when page is fully loaded or in those scripts, which are included after that partial. In case if you'd like to use internationalization anywhere, you'll have to include i18n javascript separately and include all other javascript code only after it.

Now we should configure javascript locales. We will create config file for that:

# config/i18n-js.yml
translations:
- file: "app/assets/javascripts/application/i18n/translations.js"
  only: '*.js.*'

In this file «- file» options says where to place compiled dictionaries for javascript (it is done manually, but we will discuss it little later). Note, that compilation is configured to use app/assets/javascripts/application folder in this example. This is because javascript assets are located there in our case. «only» option points us what keys to use for compilation. In our case these are all keys which have js sub-key, e.g.

en:
  admin:
    js:
      title: 'Sample'
  js:
    copyright: 'Another sample'
  post:
    js:
      name: 'One more sample'

Now, let's prepare localization files for our case:

# config/locales/js/en.yml
en:
  js:
    posts:
      select2:
        placeholder: 'Please, select tags'
        no-matches: 'No tags found'
# config/locales/js/ru.yml
ru:
  js:
    posts:
      select2:
        placeholder: 'Пожалуйста, укажите теги'
        no-matches: 'Тегов не найдено'

And the most interesting moment which was difficult for me to understand: you should manually run bin/rake i18n:js:export to export all dictionaries to javascript. We will get app/assets/javascripts/application/i18n/translations.js file as an output where all translations will be stored. And it'll work because it'll be automatically included with assets pipeline. So now we can easily localize our client-side code. Let's extend select2 initialization code to make it use locales

# app/assets/specific/posts/_form.js.coffee
$ ->
  $('#post_tag_list').select2
    tags: if gon? then gon.tags else []
    tokenSeparators: [","]
    placeholder: I18n.t('js.posts.select2.placeholder')
    formatNoMatches: (term) ->
      I18n.t('js.posts.select2.no-matches')
    width: '200'

That is pretty much all!

By the way, i18n-js is pretty independant, thus we can use it with other server-side languages.