Shahzad Bhatti

April 12, 2007

Resource Bundle in Ruby

Filed under: Computing — admin @ 5:09 pm

I have been doing Java programming for over ten years and when I work with Rails, I miss some of the conventions Java has. One of those convention is to use resource bundles for all user messages. Java provides ResourceBundle for that, but other MVC frameworks such as Struts and Spring MVC provides nice support for that. One of the thing that I like in Ruby is expressive and succint syntax, so it took me about an hour to whip up the functionality I needed. I wanted to use the resources file using method invocation or hash syntax, e.g.

MessageResource.my_message

Optionally I could pass in arguments, e.g. if I had message like:

errors_range: "{0} is not in the range {1} through {2}."

which takes in three arguments then I can invoke:

MessageResource.errors_range('credit-card', 10, 20, 30)

In addition, I could use hash access syntax, e.g.

MessageResource[:errors_range, 'credit-card', 10, 20, 30]

The other thing I needed was that if I specify locale then it
should find the message from that locale, e.g.

MessageResource.es.errors_range('credit-card', 10, 20, 30)

Finally, I wanted this to be robust so that if the key-code for the
message or locale is not found then it returns key-code instead of
throwing exception or passing nil back (this can waste a lot of time
when developing web applications.)

So without further delay, here is the code:

 1 require 'yaml'
 2 require 'resource_bundle.rb'
 3 
 4 class MessageResource
 5   @@bundles = {}
 6   def self.[](code, *args)
 7     self.populate unless @@bundles.size > 0
 8     key = code.to_s
 9     if key.length == 2 and @@bundles.has_key? key
10        bundle = @@bundles[key] || "??#{code}-#{args}??"
11     else
12        bundle = @@bundles['en']
13        if key.length == 2
14           bundle
15        else
16           bundle[key, *args] || "??#{code}??"
17        end
18      end
19   end
20 
21   ###################
22   # Populates all resource bundles and stores them by locale.
23   ###################
24   def self.populate
25     unless @@bundles.size > 0
26     files = Dir.glob(File.dirname(__FILE__) + "/../../../config/messages*")
27     files.each do |f|
28       locale = 'en'
29       locale = $&.slice(1,2) if f =~ /_...yml/
30       begin
31         messages = YAML.load_file(f)
32       rescue ArgumentError => e
33         raise ArgumentError, "Invalid resource file #{f} -- #{e}", caller
34       end
35       raise ArgumentError, "Invalid resource file #{f}", caller if !messages
36       @@bundles[locale] = ResourceBundle.new(locale, messages)
37     end
38   end
39   end
40 
41   ###################
42   # Defines method_missing for class that simply invokes array operator.
43   ###################
44   def self.method_missing(sym, *args)
45     self[sym, *args]
46   end
47 
48   protected
49   def initialize;end
50 end
51 
52 #
53 #################################################################
54 class ResourceBundle
55   def initialize(locale, messages)
56     @locale = locale
57     @messages = messages
58   end
59   #
60   ###################
61   # Overloads brackets to access messages by keys.
62   ###################
63   def [](code, *args)
64     message = @messages[code.to_s]
65     if message && args
66     args.each_with_index {|arg, i|
67       message.gsub!(/{#{i}}/, arg.to_s)
68     }
69   end
70   message || "???#{code}-#{args.join(',')}???"
71   end
72   ###################
73   # Defines method_missing for this instance that simply invokes array operator.
74   ###################
75   def method_missing(key, *args)
76     self[key, *args]
77   end
78 end

Note, I am using YAML syntax to store messages, here is a sample file:

notice_password_changed_wrong: “Your old password did not match, please try again.”
notice_password_mismatch: “Your new password did not match confirmed password, please try again.”
notice_password_changed: “Password was changed successfully.”
notice_password_reset_failed: “Password could not be reset, please try again.”
notice_password_reset_success: “Password was reset successfully.”

No Comments

No comments yet.

RSS feed for comments on this post. TrackBack URL

Sorry, the comment form is closed at this time.

Powered by WordPress