Monkey patching I18n.t in Rails

During the Ruby Hangout on Nov 7th, I suggested that it would be cool if instead of writing t 'path.to.my.string', one could instead write t.path.to.my.string. Josh Szmajda made the mistake of agreeing, and so I decided this would be a good challenge for me to learn some more Ruby tricks.

First of all, I poked into I18n and made a good discovery – the translate method is not particularly useful without passing parameters. This meant that I could use the case where there’s an empty list of parameters to return a Funky Object(TM) that would do something cool with method_missing. Also, I decided that since :t is simply an alias for :translate, I could choose to write my own t that would defer to translate when passed args and do my own thing when it wasn’t. Basically, I could write something like:

require 'i18n'

module I18n
  class << self
    def t(*args)
      if (args.empty?)
        # Do something cool here!
      else
        translate(*args)
      end
    end
  end
end

I did some quick testing and discovered that did indeed work. Now on to the tricky part. Here’s the first version:

require 'i18n'

module I18n
  class << self
    def t(*args)
      if (args.empty?)
        translator = Object.new
        translator.instance_variable_set(:@path, '')
        def translator.method_missing(method_id, *args)
          raise StandardError, "No args please!" unless args.empty?
          @path = "#{@path}.#{method_id.to_s}"
          answer = I18n.translate(@path)
          answer.respond_to?(:keys) ? self : answer
        end
        translator
      else
        translate(*args)
      end
    end
  end
end

This creates a new Object object, then sets the instance variable @path on it to an empty string, and then sets up method_missing on that object to handle arbitrary methods. It looks them up using I18n.translate in whatever path is currently active. If the returned thing behaves like a Hash, then we presume we’re going to need to do this again, and so we just return self so the next method call can follow along. On the other hand, if we gotten to something that doesn’t look like a Hash, then we just return that and we’re done.

The only problem with this approach is that there are a whole bunch of methods defined in Object that can’t be used as keys. For instance, if you have the key inspect in your locale file, you’re out of luck getting to it with this notation.

So I decided to try to swap in BasicObject for Object. This took me a little longer to figure out since I still needed a way to call instance_variable_set. I finally came up with the following. It may not be the best way to handle this, but it’s what I stumbled upon.

module I18n
  class << self
    def t(*args)
      if (args.empty?)
        translator = BasicObject.new
        def translator.__instance_variable_set(sym, obj)
          ::Object.instance_variable_set(sym, obj)
        end
        translator.__instance_variable_set(:@path, '')
        def translator.method_missing(method_id, *args)
          raise StandardError, "No args please!" unless args.empty?
          @path = "#{@path}.#{method_id.to_s}"
          answer = I18n.translate(@path)
          answer.respond_to?(:keys) ? self : answer
        end
        translator
      else
        translate(*args)
      end
    end
  end
end

I add another singleton that can call ::Object.instance_variable_set for me. I have no idea what this syntax is actually doing – I stumbled upon it by looking at the documentation on BasicObject in the Pickaxe Book. But it works! When I use I18n.t.hash_name in irb I get translation missing: en.hash_name.inspect – note that .inspect on the end that results because it tries to look up the inspect message that irb sends the result!

I’m not at all sure whether this whole exercise was a good idea from a production code standpoint, but it was fun!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s