Локализация в javascript в Ruby on Rails

При разработке многоязычного приложения мы рано или поздно столкнемся с необходимостью локализации строк используемых в javascript. В Ruby on Rails для этих целей есть замечательный гем i18n-js. Мне понадобилось некоторое время, чтобы разобраться как он работает, не смотря на наличие документации. В какой-то момент я даже было подумал, что это безнадежный вариант и чуть было не бросил эту затею, хотя на самом деле все оказалось просто.

Локализуем надписи select2 из примера одной из предыдущих статей.

Сначала просто подключим мультиязычность в приложении - сделаем все по руководству Ruby on Rails.

Прописываем опции локализации в config/initializers/locale.rb

    # config/initializers/locale.rb
    # ищем файлы локализации рекурсивно в папке 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]

Настраиваем установку локали из адреса страницы в ApplicationController

# app/controllers/application_controller.rb
  before_action :set_locale

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

Добавляем локаль ко всем генерируемым ссылкам

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

И, наконец, задаем локаль в роутах

# 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

А магия редиректов, к которой мы прибегли выше, подробнее описывается в другом моем посте.

Добавим переключение локали в лейаут, используя специальный хелпер

# 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 %>

Обычно я дополнительно использую гем rails-i18n для некоторых базовых локализаций, однако в данном примере он нам не понадобится. Подключаем гем i18n-js в Gemfile. Ставим гемы через bundler, выполнив команду bundle install

Добавляем клиентские скрипты, обеспечивающие локализацию в javascript

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

Создадим partial лейаут, в котором настройки локализации с сервеной части будем передавать в клиентскую часть

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

И подключаем этот partial в основном лейауте в разделе head после подключения общих js.

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

При этом стоит отметить, что в таком случае локализацию мы сможем использовать только после загрузки страницы или в тех файлах, которые были подключены после нашего partial'а. Если же мы хотим использовать локализацию вообще везде и сразу, то нам нужно будет отдельно подключить javascript i18n и сразу после этого подключить настройки локализации и только потом — вcе остальные скрипты.

Теперь нам осталось настроить яваскриптовые локали. Для этого создаем файл конфигурации:

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

В данном файле директивой - file мы указываем путь, куда будут складывать компилируемые словари переводов для js (их нужно компилировать вручную, об этом позже). Стоит отметить, что в данном примере указана компиляция в папку app/assets/javascripts/application, поскольку в нашей конфигурации из этой папки собираются все ассеты. Директива only указывает какие ключи в файлах переводов будут компилироваться в яваскриптовый словарь переводов. В нашем примере собираются все ключи, в которых встречается подключ js, примеры:

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

Итак, подготовим файлы локализации для нашего случая:

# 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: 'Тегов не найдено'

И самый интересный момент, который я долго не мог понять: чтобы экспортировать локали в словари js - нужно запустить вручную команду bin/rake i18n:js:export. После ее выполнения мы получим javascript файл app/assets/javascripts/application/i18n/translations.js, в котором и будет храниться наш словарь с переводами. И работать он будет, поскольку он будет автоматически подключаться через механизм ассетов. Ну и теперь мы наконец можем использовать локализацию в клиентских скриптах. Расширим инициализацию select2 для использования локалей

# 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'

Готово!

К слову сказать, i18n-js вполне независимая библиотека, поэтому данный подход может применяться и в случае использования других языков программирования на стороне сервера.