Rails I18n and elegant message passing to Javascript

Software Engineer

Internationalization is the process of abstracting all strings out of your application. Rails provides excellent support for Internationalization.

Rails’ internationalization works well when views are built the Rails way. But when we are looking to build SPAs and front-end heavy applications, we often end up hard-coding error messages, response status messages (etc) in our javascript fies. Or worse, polluting javascript with Rails helpers!

$(document).ready(function(){
  alert("Welcome!");
  alert("<%= t('welcome_message') %>"); // I hope no one finds this!
})

As our projects grow, such practices contribute to greater software entropy and it becomes nearly impossible to maintain code.

Keeping it neat and clean


1.The MessageHandler service.

Write a simple message handler service which takes in a language, parses the corresponding language.yml file (from config/locales/*.yml) and returns it’s contents as a hash.

class MessageHandler
 class << self

  @@data = {}
  # It doesn't make sense to parse the YML file everytime.
  # The results are memoized in @@data

  def data_in(language)
    load! if !@@data[language]
    @@data[language][language.to_s]
  end

  def load!
    possible_languages.each do |language|
      file_path = locale_file_path(language)
      @@data[language] ||= YAML.load(File.read(file_path)
    end
  end

  def possible_languages
    [ :en, :es, :pt]
  end  

  def locale_file_path(language)
    File.join(Rails.root, 'config', 'locales', "#{language.to_s}.yml")
  end

 end
end

2.Using gon.

Gon helps you streamline data transfer to javascript. You can set data in your controller, gon will make it available in your javascript.

Use gon to set the contents of your language files to a javascript variable, in the application conroller.

#EN.YML
welcome_message: 'Welcome!'
class ApplicationController < ActionController::Base
  before_filter :set_messages

private
  def set_messages
    I18n.locale = params[:locale] || I18n.default_locale
    gon.messages = MessageHandler.data_in(I18n.locale)
  end
end
<%= include_gon(:camel_case => true, :namespace => 'app', :init => true) %>

<script>
  console.log(app.messages); // '{ welcome_message: "Welcome!" }'
</script>

3.Some Optimization

While this approach works great, when ever we add/modify our YAML files we need to restart our Rails sever for it reflect in our javascript.

Everytime our Language-YAML files are changed, load! has to be invoked. This can be done by using Rails’ ActiveSupport::FileUpdateChecker service which Rails uses to reload code dynamically during runtime.

def data_in(language)
  load! if !@@data[language] or files_changed?
  @@data[language][language.to_s]
end

def files_changed?
  @@file_update_checker ||= ActiveSupport::FileUpdateChecker.new(language_file_paths)
  @@file_update_checker.updated?
end

4.Voilà

Everything just works!

// /?locale=en
alert(app.messages.welcome_message); // 'Welcome!'

// /?locale=fr
alert(app.messages.welcome_message); // 'Accueil!'

// /?locale=tm
alert(app.messages.welcome_message); // 'வரவேற்பு!'

References

Comments

  • You don’t cover the case when two or more files used for one language. This is typical if you use external gems that provide localizations for you (e.g. Devise, Formtastic, Rails-i18n, etc) and you want to reuse these messages in your app, or streamline the error message keys to your JS frontend instead of the full message.

Parallel processing in ruby
Read
Open github network graph from command line
Read