svenfuchs-i18n 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ h1. Changelog
2
+
3
+ h2. master
4
+
5
+ * (no changes)
6
+
7
+ h2. 0.2.0 (2009-07-12, "fb9819c7efd3713c178c47b0722d0b39fff14131":http://github.com/svenfuchs/i18n/commit/fb9819c7efd3713c178c47b0722d0b39fff14131)
8
+
9
+ * "Allow using Ruby 1.9 syntax for string interpolation (API addition)":http://github.com/svenfuchs/i18n/commit/c6e0b06d512f2af57199a843a1d8a40241b32861
10
+ * "Allow configuring the default scope separator, allow to pass a custom scope separator (e.g. I18n.t(:'foo|bar', :separator => '|') (API addition)":http://github.com/svenfuchs/i18n/commit/5b75bfbc348061adc11e3790187a187275bfd471
11
+ * "Pass :format option to #translate for #localize more useful lambda support":http://github.com/svenfuchs/i18n/commit/e277711b3c844fe7589b8d3f9af0f7d1b969a273
12
+ * "Refactor Simple backend #resolve to #default and #resolve for more consistency. Now allows to pass lambdas as defaults and re-resolve Symbols":http://github.com/svenfuchs/i18n/commit/8c4ce3d923ce5fa73e973fe28217e18165549aba
13
+ * "Add lambda support to #translate (API addition)":http://github.com/svenfuchs/i18n/commit/c90e62d8f7d3d5b78f34cfe328d871b58884f115
14
+ * "Add lambda support to #localize (API addition)":http://github.com/svenfuchs/i18n/commit/9d390afcf33f3f469bb95e6888147152f6cc7442
15
+
16
+ h2. 0.1.3 (2009-02-27)
17
+
18
+ * "Remove unnecessary string encoding handling in the i18n simple backend which made the backend break on Ruby 1.9":http://github.com/svenfuchs/i18n/commit/4c3a970783861a94f2e89f46714fb3434e4f4f8d
19
+
20
+ h2. 0.1.2 (2009-01-09)
21
+
22
+ * "added #available_locales (returns an array of locales for which translations are available)":http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4
23
+ * "flatten load_path before using it so that a nested array of paths won't throw up":http://github.com/svenfuchs/i18n/commit/d473a068a2b90aba98135deb225d6eb6d8104d70
24
+
25
+ h2. 0.1.1 (2008-11-20)
26
+
27
+ * "Use :'en' as a default locale (in favor of :'en-US')":http://github.com/svenfuchs/i18n/commit/c4b10b246aecf7da78cb2568dd0d2ab7e6b8a230
28
+ * "Add #reload! to Simple backend":http://github.com/svenfuchs/i18n/commit/36dd2bd9973b9e1559728749a9daafa44693e964
29
+
30
+ h2. 0.1.0 (2008-10-25)
31
+
32
+ * "Fix Simple backend to distinguish false from nil values":http://github.com/svenfuchs/i18n/commit/39d9a47da14b5f3ba126af48923af8c30e135166
33
+ * "Add #load_path to public api, add initialize to simple backend and remove #load_translations from public api":http://github.com/svenfuchs/i18n/commit/c4c5649e6bc8f020f1aaf5a5470bde048e22c82d
34
+ * "Speed up Backend::Simple#interpolate":http://github.com/svenfuchs/i18n/commit/9e1ac6bf8833304e036323ec9932b9f33c468a35
35
+ * "Remove #populate and #store_translations from public API":http://github.com/svenfuchs/i18n/commit/f4e514a80be7feb509f66824ee311905e2940900
36
+ * "Use :other instead of :many as a plural key":http://github.com/svenfuchs/i18n/commit/0f8f20a2552bf6a2aa758d8fdd62a7154e4a1bf6
37
+ * "Use a class instead of a module for Simple backend":http://github.com/svenfuchs/i18n/commit/08f051aa61320c17debde24a83268bc74e33b995
38
+ * "Make Simple backend #interpolate deal with non-ASCII string encodings":http://github.com/svenfuchs/i18n/commit/d84a3f3f55543c084d5dc5d1fed613b8df148789
39
+ * "Fix default arrays of non-existant keys returning the default array":http://github.com/svenfuchs/i18n/commit/6c04ca86c87f97dc78f07c2a4023644e5ba8b839
40
+
41
+ h2. Initial implementation (June/July 2008)
42
+
43
+ Initial implementation by "Sven Fuchs":http://www.workingwithrails.com/person/9963-sven-fuchs based on previous discussion/consensus of the rails-i18n team (alphabetical order) and many others:
44
+
45
+ * "Matt Aimonetti":http://railsontherun.com
46
+ * "Sven Fuchs":http://www.workingwithrails.com/person/9963-sven-fuchs
47
+ * "Joshua Harvey":http://www.workingwithrails.com/person/759-joshua-harvey
48
+ * "Saimon Moore":http://saimonmoore.net
49
+ * "Stephan Soller":http://www.arkanis-development.de
50
+
51
+ h2. More information
52
+
53
+ * "Homepage":http://rails-i18n.org
54
+ * "Wiki":http://rails-i18n.org/wiki
55
+ * "Mailinglist":http://groups.google.com/group/rails-i18n
56
+ * "About the project/history":http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized
57
+ * "Initial API Intro":http://www.artweb-design.de/2008/7/18/the-ruby-on-rails-i18n-core-api
@@ -0,0 +1,20 @@
1
+ task :default => [:test]
2
+
3
+ task :test do
4
+ ruby "test/all.rb"
5
+ end
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |s|
10
+ s.name = "i18n"
11
+ s.summary = "New wave Internationalization support for Ruby"
12
+ s.email = "rails-i18n@googlegroups.com"
13
+ s.homepage = "http://rails-i18n.org"
14
+ s.description = "Add Internationalization support to your Ruby application."
15
+ s.authors = ['Sven Fuchs', 'Joshua Harvey', 'Matt Aimonetti', 'Stephan Soller', 'Saimon Moore']
16
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -7,11 +7,13 @@
7
7
  # License:: MIT
8
8
  require 'i18n/backend/simple'
9
9
  require 'i18n/exceptions'
10
+ require 'i18n/string'
10
11
 
11
12
  module I18n
12
13
  @@backend = nil
13
14
  @@load_path = nil
14
15
  @@default_locale = :'en'
16
+ @@default_separator = '.'
15
17
  @@exception_handler = :default_exception_handler
16
18
 
17
19
  class << self
@@ -50,6 +52,16 @@ module I18n
50
52
  backend.available_locales
51
53
  end
52
54
 
55
+ # Returns the current default scope separator. Defaults to '.'
56
+ def default_separator
57
+ @@default_separator
58
+ end
59
+
60
+ # Sets the current default scope separator.
61
+ def default_separator=(separator)
62
+ @@default_separator = separator
63
+ end
64
+
53
65
  # Sets the exception handler.
54
66
  def exception_handler=(exception_handler)
55
67
  @@exception_handler = exception_handler
@@ -150,7 +162,7 @@ module I18n
150
162
  # or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
151
163
  # I18n.t :foo, :default => [:bar, 'default']
152
164
  #
153
- # <b>BULK LOOKUP</b>
165
+ # *BULK LOOKUP*
154
166
  #
155
167
  # This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
156
168
  # I18n.t [:foo, :bar]
@@ -160,8 +172,26 @@ module I18n
160
172
  #
161
173
  # Which is the same as using a scope option:
162
174
  # I18n.t [:foo, :bar], :scope => :baz
163
- def translate(key, options = {})
164
- locale = options.delete(:locale) || I18n.locale
175
+ #
176
+ # *LAMBDAS*
177
+ #
178
+ # Both translations and defaults can be given as Ruby lambdas. Lambdas will be
179
+ # called and passed the key and options.
180
+ #
181
+ # E.g. assuming the key <tt>:salutation</tt> resolves to:
182
+ # lambda { |key, options| options[:gender] == 'm' ? "Mr. {{options[:name]}}" : "Mrs. {{options[:name]}}" }
183
+ #
184
+ # Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
185
+ #
186
+ # It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
187
+ # a cache layer is put in front of I18n.translate it will generate a cache key
188
+ # from the argument values passed to #translate. Therefor your lambdas should
189
+ # always return the same translations/values per unique combination of argument
190
+ # values.
191
+ def translate(*args)
192
+ options = args.last.is_a?(Hash) ? args.pop : {}
193
+ key = args.shift
194
+ locale = options.delete(:locale) || I18n.locale
165
195
  backend.translate(locale, key, options)
166
196
  rescue I18n::ArgumentError => e
167
197
  raise e if options[:raise]
@@ -190,9 +220,9 @@ module I18n
190
220
  # Merges the given locale, key and scope into a single array of keys.
191
221
  # Splits keys that contain dots into multiple keys. Makes sure all
192
222
  # keys are Symbols.
193
- def normalize_translation_keys(locale, key, scope)
194
- keys = [locale] + Array(scope) + [key]
195
- keys = keys.map { |k| k.to_s.split(/\./) }
223
+ def normalize_translation_keys(locale, key, scope, separator = nil)
224
+ keys = [locale] + Array(scope) + Array(key)
225
+ keys = keys.map { |k| k.to_s.split(separator || I18n.default_separator) }
196
226
  keys.flatten.map { |k| k.to_sym }
197
227
  end
198
228
  end
@@ -3,8 +3,8 @@ require 'yaml'
3
3
  module I18n
4
4
  module Backend
5
5
  class Simple
6
- INTERPOLATION_RESERVED_KEYS = %w(scope default)
7
- MATCH = /(\\\\)?\{\{([^\}]+)\}\}/
6
+ RESERVED_KEYS = [:scope, :default, :separator]
7
+ INTERPOLATION_SYNTAX_PATTERN = /(\\\\)?\{\{([^\}]+)\}\}/
8
8
 
9
9
  # Accepts a list of paths to translation files. Loads translations from
10
10
  # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
@@ -23,20 +23,15 @@ module I18n
23
23
 
24
24
  def translate(locale, key, options = {})
25
25
  raise InvalidLocale.new(locale) if locale.nil?
26
- return key.map { |k| translate(locale, k, options) } if key.is_a? Array
27
-
28
- reserved = :scope, :default
29
- count, scope, default = options.values_at(:count, *reserved)
30
- options.delete(:default)
31
- values = options.reject { |name, value| reserved.include?(name) }
32
-
33
- entry = lookup(locale, key, scope)
34
- if entry.nil?
35
- entry = default(locale, default, options)
36
- if entry.nil?
37
- raise(I18n::MissingTranslationData.new(locale, key, options))
38
- end
39
- end
26
+ return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
27
+
28
+ count, scope, default, separator = options.values_at(:count, *RESERVED_KEYS)
29
+ values = options.reject { |name, value| RESERVED_KEYS.include?(name) }
30
+
31
+ entry = lookup(locale, key, scope, separator)
32
+ entry = entry.nil? ? default(locale, key, default, options) : resolve(locale, key, entry, options)
33
+
34
+ raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
40
35
  entry = pluralize(locale, entry, count)
41
36
  entry = interpolate(locale, entry, values)
42
37
  entry
@@ -45,23 +40,23 @@ module I18n
45
40
  # Acts the same as +strftime+, but returns a localized version of the
46
41
  # formatted date string. Takes a key from the date/time formats
47
42
  # translations as a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
48
- def localize(locale, object, format = :default)
43
+ def localize(locale, object, format = :default, options={})
49
44
  raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
50
45
 
51
- type = object.respond_to?(:sec) ? 'time' : 'date'
52
- # TODO only translate these if format is a String?
53
- formats = translate(locale, :"#{type}.formats")
54
- format = formats[format.to_sym] if formats && formats[format.to_sym]
55
- # TODO raise exception unless format found?
56
- format = format.to_s.dup
57
-
58
- # TODO only translate these if the format string is actually present
59
- # TODO check which format strings are present, then bulk translate then, then replace them
60
- format.gsub!(/%a/, translate(locale, :"date.abbr_day_names")[object.wday])
61
- format.gsub!(/%A/, translate(locale, :"date.day_names")[object.wday])
62
- format.gsub!(/%b/, translate(locale, :"date.abbr_month_names")[object.mon])
63
- format.gsub!(/%B/, translate(locale, :"date.month_names")[object.mon])
64
- format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}")) if object.respond_to? :hour
46
+ if Symbol === format
47
+ type = object.respond_to?(:sec) ? 'time' : 'date'
48
+ format = lookup(locale, :"#{type}.formats.#{format}")
49
+ end
50
+
51
+ format = resolve(locale, object, format, options.merge(:raise => true))
52
+
53
+ # TODO check which format strings are present, then bulk translate them, then replace them
54
+ format.gsub!(/%a/, translate(locale, :"date.abbr_day_names", :format => format)[object.wday]) if format.include?('%a')
55
+ format.gsub!(/%A/, translate(locale, :"date.day_names", :format => format)[object.wday]) if format.include?('%A')
56
+ format.gsub!(/%b/, translate(locale, :"date.abbr_month_names", :format => format)[object.mon]) if format.include?('%b')
57
+ format.gsub!(/%B/, translate(locale, :"date.month_names", :format => format)[object.mon]) if format.include?('%B')
58
+ format.gsub!(/%p/, translate(locale, :"time.#{object.hour < 12 ? :am : :pm}", :format => format)) if format.include?('%p') && object.respond_to?(:hour)
59
+
65
60
  object.strftime(format)
66
61
  end
67
62
 
@@ -95,10 +90,10 @@ module I18n
95
90
  # nested translations hash. Splits keys or scopes containing dots
96
91
  # into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
97
92
  # <tt>%w(currency format)</tt>.
98
- def lookup(locale, key, scope = [])
93
+ def lookup(locale, key, scope = [], separator = nil)
99
94
  return unless key
100
95
  init_translations unless initialized?
101
- keys = I18n.send(:normalize_translation_keys, locale, key, scope)
96
+ keys = I18n.send(:normalize_translation_keys, locale, key, scope, separator)
102
97
  keys.inject(translations) do |result, k|
103
98
  if (x = result[k.to_sym]).nil?
104
99
  return nil
@@ -108,20 +103,34 @@ module I18n
108
103
  end
109
104
  end
110
105
 
111
- # Evaluates a default translation.
112
- # If the given default is a String it is used literally. If it is a Symbol
113
- # it will be translated with the given options. If it is an Array the first
114
- # translation yielded will be returned.
115
- #
116
- # <em>I.e.</em>, <tt>default(locale, [:foo, 'default'])</tt> will return +default+ if
117
- # <tt>translate(locale, :foo)</tt> does not yield a result.
118
- def default(locale, default, options = {})
119
- case default
120
- when String then default
121
- when Symbol then translate locale, default, options
122
- when Array then default.each do |obj|
123
- result = default(locale, obj, options.dup) and return result
106
+ # Evaluates defaults.
107
+ # If given subject is an Array, it walks the array and returns the
108
+ # first translation that can be resolved. Otherwise it tries to resolve
109
+ # the translation directly.
110
+ def default(locale, object, subject, options = {})
111
+ options = options.dup.reject { |key, value| key == :default }
112
+ case subject
113
+ when Array
114
+ subject.each do |subject|
115
+ result = resolve(locale, object, subject, options) and return result
124
116
  end and nil
117
+ else
118
+ resolve(locale, object, subject, options)
119
+ end
120
+ end
121
+
122
+ # Resolves a translation.
123
+ # If the given subject is a Symbol, it will be translated with the
124
+ # given options. If it is a Proc then it will be evaluated. All other
125
+ # subjects will be returned directly.
126
+ def resolve(locale, object, subject, options = {})
127
+ case subject
128
+ when Symbol
129
+ translate(locale, subject, options)
130
+ when Proc
131
+ resolve(locale, object, subject.call(object, options), options = {})
132
+ else
133
+ subject
125
134
  end
126
135
  rescue MissingTranslationData
127
136
  nil
@@ -133,7 +142,7 @@ module I18n
133
142
  # implement more flexible or complex pluralization rules.
134
143
  def pluralize(locale, entry, count)
135
144
  return entry unless entry.is_a?(Hash) and count
136
- # raise InvalidPluralizationData.new(entry, count) unless entry.is_a?(Hash)
145
+
137
146
  key = :zero if count == 0 && entry.has_key?(:zero)
138
147
  key ||= count == 1 ? :one : :other
139
148
  raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
@@ -149,21 +158,22 @@ module I18n
149
158
  # the <tt>{{...}}</tt> key in a string (once for the string and once for the
150
159
  # interpolation).
151
160
  def interpolate(locale, string, values = {})
152
- return string unless string.is_a?(String)
161
+ return string unless string.is_a?(String) && !values.empty?
153
162
 
154
- string.gsub(MATCH) do
155
- escaped, pattern, key = $1, $2, $2.to_sym
163
+ string.gsub(INTERPOLATION_SYNTAX_PATTERN) do
164
+ escaped, key = $1, $2.to_sym
156
165
 
157
166
  if escaped
158
- pattern
159
- elsif INTERPOLATION_RESERVED_KEYS.include?(pattern)
160
- raise ReservedInterpolationKey.new(pattern, string)
161
- elsif !values.include?(key)
162
- raise MissingInterpolationArgument.new(pattern, string)
167
+ key
168
+ elsif RESERVED_KEYS.include?(key)
169
+ raise ReservedInterpolationKey.new(key, string)
163
170
  else
164
- values[key].to_s
171
+ "%{#{key}}"
165
172
  end
166
- end
173
+ end % values
174
+
175
+ rescue KeyError => e
176
+ raise MissingInterpolationArgument.new(values, string)
167
177
  end
168
178
 
169
179
  # Loads a single translations file by delegating to #load_rb or
@@ -204,11 +214,20 @@ module I18n
204
214
  # Return a new hash with all keys and nested keys converted to symbols.
205
215
  def deep_symbolize_keys(hash)
206
216
  hash.inject({}) { |result, (key, value)|
207
- value = deep_symbolize_keys(value) if value.is_a? Hash
217
+ value = deep_symbolize_keys(value) if value.is_a?(Hash)
208
218
  result[(key.to_sym rescue key) || key] = value
209
219
  result
210
220
  }
211
221
  end
222
+
223
+ # Flatten the given array once
224
+ def flatten_once(array)
225
+ result = []
226
+ for element in array # a little faster than each
227
+ result.push(*element)
228
+ end
229
+ result
230
+ end
212
231
  end
213
232
  end
214
233
  end
@@ -1,3 +1,9 @@
1
+ class KeyError < IndexError
2
+ def initialize(message = nil)
3
+ super(message || "key not found")
4
+ end
5
+ end unless defined?(KeyError)
6
+
1
7
  module I18n
2
8
  class ArgumentError < ::ArgumentError; end
3
9
 
@@ -28,10 +34,10 @@ module I18n
28
34
  end
29
35
 
30
36
  class MissingInterpolationArgument < ArgumentError
31
- attr_reader :key, :string
32
- def initialize(key, string)
33
- @key, @string = key, string
34
- super "interpolation argument #{key} missing in #{string.inspect}"
37
+ attr_reader :values, :string
38
+ def initialize(values, string)
39
+ @values, @string = values, string
40
+ super "missing interpolation argument in #{string.inspect} (#{values.inspect} given)"
35
41
  end
36
42
  end
37
43
 
@@ -0,0 +1,93 @@
1
+ =begin
2
+ heavily based on Masao Mutoh's gettext String interpolation extension
3
+ http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
4
+ Copyright (C) 2005-2009 Masao Mutoh
5
+ You may redistribute it and/or modify it under the same license terms as Ruby.
6
+ =end
7
+
8
+ if RUBY_VERSION < '1.9'
9
+
10
+ # KeyError is raised by String#% when the string contains a named placeholder
11
+ # that is not contained in the given arguments hash. Ruby 1.9 includes and
12
+ # raises this exception natively. We define it to mimic Ruby 1.9's behaviour
13
+ # in Ruby 1.8.x
14
+
15
+ class KeyError < IndexError
16
+ def initialize(message = nil)
17
+ super(message || "key not found")
18
+ end
19
+ end unless defined?(KeyError)
20
+
21
+ # Extension for String class. This feature is included in Ruby 1.9 or later but not occur TypeError.
22
+ #
23
+ # String#% method which accept "named argument". The translator can know
24
+ # the meaning of the msgids using "named argument" instead of %s/%d style.
25
+
26
+ class String
27
+ # For older ruby versions, such as ruby-1.8.5
28
+ alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'}
29
+ alias :interpolate_without_ruby_19_syntax :% # :nodoc:
30
+
31
+ INTERPOLATION_PATTERN = Regexp.union(
32
+ /%%/,
33
+ /%\{(\w+)\}/, # matches placeholders like "%{foo}"
34
+ /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
35
+ )
36
+
37
+ # % uses self (i.e. the String) as a format specification and returns the
38
+ # result of applying it to the given arguments. In other words it interpolates
39
+ # the given arguments to the string according to the formats the string
40
+ # defines.
41
+ #
42
+ # There are three ways to use it:
43
+ #
44
+ # * Using a single argument or Array of arguments.
45
+ #
46
+ # This is the default behaviour of the String class. See Kernel#sprintf for
47
+ # more details about the format string.
48
+ #
49
+ # Example:
50
+ #
51
+ # "%d %s" % [1, "message"]
52
+ # # => "1 message"
53
+ #
54
+ # * Using a Hash as an argument and unformatted, named placeholders.
55
+ #
56
+ # When you pass a Hash as an argument and specify placeholders with %{foo}
57
+ # it will interpret the hash values as named arguments.
58
+ #
59
+ # Example:
60
+ #
61
+ # "%{firstname}, %{lastname}" % {:firstname => "Masao", :lastname => "Mutoh"}
62
+ # # => "Masao Mutoh"
63
+ #
64
+ # * Using a Hash as an argument and formatted, named placeholders.
65
+ #
66
+ # When you pass a Hash as an argument and specify placeholders with %<foo>d
67
+ # it will interpret the hash values as named arguments and format the value
68
+ # according to the formatting instruction appended to the closing >.
69
+ #
70
+ # Example:
71
+ #
72
+ # "%<integer>d, %<float>.1f" % { :integer => 10, :float => 43.4 }
73
+ # # => "10, 43.3"
74
+ def %(args)
75
+ if args.kind_of?(Hash)
76
+ dup.gsub(INTERPOLATION_PATTERN) do |match|
77
+ if match == '%%'
78
+ '%'
79
+ else
80
+ key = ($1 || $2).to_sym
81
+ raise KeyError unless args.has_key?(key)
82
+ $3 ? sprintf("%#{$3}", args[key]) : args[key]
83
+ end
84
+ end
85
+ elsif self =~ INTERPOLATION_PATTERN
86
+ raise ArgumentError.new('one hash required')
87
+ else
88
+ result = gsub(/%([{<])/, '%%\1')
89
+ result.send :'interpolate_without_ruby_19_syntax', args
90
+ end
91
+ end
92
+ end
93
+ end