Thomas Fuchs
Hi, I'm Thomas Fuchs. I'm the author of Zepto.js, of script.aculo.us, and I'm a Ruby on Rails core alumnus. With Amy Hoy I'm building cheerful software, like Noko Time Tracking and Every Time Zone and write books like Retinafy.me.
   Want me to speak at your conference? Contact me!

Ruby on Rails i18n revisited

October 3rd, 2005

Having a requirement for both internationalization and per-instance customizability of translations in our soon to be revealed application, I wanted to go with an as-simple-as-possible pure Ruby solution for translating strings.

While gettext based approaches might have some advantages over this (some tools available, possibly faster speed with C-based variants) I didn’t want to go with a sledgehammerized solution (I’ve very few strings in the app, and I really only need to support a handful of languages), so I’ve come up with this:


# .rb files to define l10ns in (lang/ and lang/custom/)
Localization.define('de_DE') do |l|
  l.store "blah", "blub"
  l.store "testing %d", ["Singular: %d", "Plural: %d"]
end

# Call from anywhere (extension to Object class):
_('blah')
_('testing %d', 5)

# in .rhtml
<%=_ 'testing %d', 1 %>

# current language is a class var in class
# Localization, so set e.g. in application.rb
Localization.lang = 'de_DE'

# in environment.rb (rails 0.13.1)
Localization.load

All you need to include for this is the following localization.rb file, which you stick in your lib directory:


module Localization
  mattr_accessor :lang

  @@l10s = { :default => {} }
  @@lang = :default

  def self._(string_to_localize, *args)
    translated =
      @@l10s[@@lang][string_to_localize] || string_to_localize
    return translated.call(*args).to_s if translated.is_a? Proc
    translated =
      translated[args[0]>1 ? 1 : 0] if translated.is_a?(Array)
    sprintf translated, *args
  end

  def self.define(lang = :default)
    @@l10s[lang] ||= {}
    yield @@l10s[lang]
  end

  def self.load
    Dir.glob("#{RAILS_ROOT}/lang/*.rb"){ |t| require t }
    Dir.glob("#{RAILS_ROOT}/lang/custom/*.rb"){ |t| require t }
  end

end

class Object
  def _(*args); Localization._(*args); end
end

It should be easy to alter or extend that for your own purposes, or just use it as-is.

Update: Changed to module; and here’s a very quick hack to extract a nice pre-generated guesstimation of a l10n file:


# Generates a best-estimate l10n file from all views by
# collecting calls to _() -- note: use the generated file only
# as a start (this method is only guesstimating)
def self.generate_l10n_file
  "Localization.define('en_US') do |l|n" <<
  Dir.glob("#{RAILS_ROOT}/app/views/**/*.rhtml").collect do |f|
    ["# #{f}"] << File.read(f).scan(/<%.*[^w]_s*["'](.*?)["']/)
  end.uniq.flatten.collect do |g|
    g.starts_with?('#') ? "n  #{g}" : "  l.store '#{g}', '#{g}'"
  end.uniq.join("n") << "nend"
end

To use that, call up script/console and do a puts Localization.generate_l10n_file.

Update 2: I’ve added support for using nice lambda blocks of code for those “speciality” translations. The block gets passed the *args given to the _ method, so you can basically do anything:


Localization.define do |l|
   l.store '(time)', lambda { |t| t.strftime('%I:%M%p') }
end

Localization.define('de_DE') do |l|
   l.store '(time)', lambda { |t| t.strftime('%H:%M') }
end

_ '(time)', Time.now =>"10:13PM"
Localization.lang = 'de_DE'
_ '(time)', Time.now => "22:13"