diff --git a/lib/i18n/core_ext/string/interpolate.rb b/lib/i18n/core_ext/string/interpolate.rb index e69de29b..24a6fd9a 100644 --- a/lib/i18n/core_ext/string/interpolate.rb +++ b/lib/i18n/core_ext/string/interpolate.rb @@ -0,0 +1,96 @@ +=begin + heavily based on Masao Mutoh's gettext String interpolation extension + http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb + Copyright (C) 2005-2009 Masao Mutoh + You may redistribute it and/or modify it under the same license terms as Ruby. +=end + +begin + raise ArgumentError if ("a %{x}" % {:x=>'b'}) != 'a b' +rescue ArgumentError + # KeyError is raised by String#% when the string contains a named placeholder + # that is not contained in the given arguments hash. Ruby 1.9 includes and + # raises this exception natively. We define it to mimic Ruby 1.9's behaviour + # in Ruby 1.8.x + class KeyError < IndexError + def initialize(message = nil) + super(message || "key not found") + end + end unless defined?(KeyError) + + # Extension for String class. This feature is included in Ruby 1.9 or later but not occur TypeError. + # + # String#% method which accept "named argument". The translator can know + # the meaning of the msgids using "named argument" instead of %s/%d style. + class String + # For older ruby versions, such as ruby-1.8.5 + alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'} + alias :interpolate_without_ruby_19_syntax :% # :nodoc: + + INTERPOLATION_PATTERN = Regexp.union( + /%\{(\w+)\}/, # matches placeholders like "%{foo}" + /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%.d" + ) + + INTERPOLATION_PATTERN_WITH_ESCAPE = Regexp.union( + /%%/, + INTERPOLATION_PATTERN + ) + + # % uses self (i.e. the String) as a format specification and returns the + # result of applying it to the given arguments. In other words it interpolates + # the given arguments to the string according to the formats the string + # defines. + # + # There are three ways to use it: + # + # * Using a single argument or Array of arguments. + # + # This is the default behaviour of the String class. See Kernel#sprintf for + # more details about the format string. + # + # Example: + # + # "%d %s" % [1, "message"] + # # => "1 message" + # + # * Using a Hash as an argument and unformatted, named placeholders. + # + # When you pass a Hash as an argument and specify placeholders with %{foo} + # it will interpret the hash values as named arguments. + # + # Example: + # + # "%{firstname}, %{lastname}" % {:firstname => "Masao", :lastname => "Mutoh"} + # # => "Masao Mutoh" + # + # * Using a Hash as an argument and formatted, named placeholders. + # + # When you pass a Hash as an argument and specify placeholders with %d + # it will interpret the hash values as named arguments and format the value + # according to the formatting instruction appended to the closing >. + # + # Example: + # + # "%d, %.1f" % { :integer => 10, :float => 43.4 } + # # => "10, 43.3" + def %(args) + if args.kind_of?(Hash) + dup.gsub(INTERPOLATION_PATTERN_WITH_ESCAPE) do |match| + if match == '%%' + '%' + else + key = ($1 || $2).to_sym + raise KeyError unless args.has_key?(key) + $3 ? sprintf("%#{$3}", args[key]) : args[key] + end + end + elsif self =~ INTERPOLATION_PATTERN + raise ArgumentError.new('one hash required') + else + result = gsub(/%([{<])/, '%%\1') + result.send :'interpolate_without_ruby_19_syntax', args + end + end + end +end diff --git a/test/core_ext/string/interpolate_test.rb b/test/core_ext/string/interpolate_test.rb new file mode 100644 index 00000000..993d454d --- /dev/null +++ b/test/core_ext/string/interpolate_test.rb @@ -0,0 +1,99 @@ +require 'test_helper' + +# thanks to Masao's String extensions these should work the same in +# Ruby 1.8 (patched) and Ruby 1.9 (native) +# some tests taken from Masao's tests +# http://github.com/mutoh/gettext/blob/edbbe1fa8238fa12c7f26f2418403015f0270e47/test/test_string.rb + +class I18nCoreExtStringInterpolationTest < Test::Unit::TestCase + test "String interpolates a single argument" do + assert_equal "Masao", "%s" % "Masao" + end + + test "String interpolates an array argument" do + assert_equal "1 message", "%d %s" % [1, 'message'] + end + + test "String interpolates a hash argument w/ named placeholders" do + assert_equal "Masao Mutoh", "%{first} %{last}" % { :first => 'Masao', :last => 'Mutoh' } + end + + test "String interpolates a hash argument w/ named placeholders (reverse order)" do + assert_equal "Mutoh, Masao", "%{last}, %{first}" % { :first => 'Masao', :last => 'Mutoh' } + end + + test "String interpolates named placeholders with sprintf syntax" do + assert_equal "10, 43.4", "%d, %.1f" % {:integer => 10, :float => 43.4} + end + + test "String interpolates named placeholders with sprintf syntax, does not recurse" do + assert_equal "%s", "%{msg}" % { :msg => '%s', :not_translated => 'should not happen' } + end + + test "String interpolation does not replace anything when no placeholders are given" do + assert_equal("aaa", "aaa" % {:num => 1}) + assert_equal("bbb", "bbb" % [1]) + end + + test "String interpolation sprintf behaviour equals Ruby 1.9 behaviour" do + assert_equal("1", "%d" % {:num => 1}) + assert_equal("0b1", "%#b" % {:num => 1}) + assert_equal("foo", "%s" % {:msg => "foo"}) + assert_equal("1.000000", "%f" % {:num => 1.0}) + assert_equal(" 1", "%3.0f" % {:num => 1.0}) + assert_equal("100.00", "%2.2f" % {:num => 100.0}) + assert_equal("0x64", "%#x" % {:num => 100.0}) + assert_raise(ArgumentError) { "%,d" % {:num => 100} } + assert_raise(ArgumentError) { "%/d" % {:num => 100} } + end + + test "String interpolation old-style sprintf still works" do + assert_equal("foo 1.000000", "%s %f" % ["foo", 1.0]) + end + + test "String interpolation raises an ArgumentError when the string has extra placeholders (Array)" do + assert_raise(ArgumentError) do # Ruby 1.9 msg: "too few arguments" + "%s %s" % %w(Masao) + end + end + + test "String interpolation raises a KeyError when the string has extra placeholders (Hash)" do + assert_raise(KeyError) do # Ruby 1.9 msg: "key not found" + "%{first} %{last}" % { :first => 'Masao' } + end + end + + test "String interpolation does not raise when passed extra values (Array)" do + assert_nothing_raised do + assert_equal "Masao", "%s" % %w(Masao Mutoh) + end + end + + test "String interpolation does not raise when passed extra values (Hash)" do + assert_nothing_raised do + assert_equal "Masao Mutoh", "%{first} %{last}" % { :first => 'Masao', :last => 'Mutoh', :salutation => 'Mr.' } + end + end + + test "% acts as escape character in String interpolation" do + assert_equal "%{first}", "%%{first}" % { :first => 'Masao' } + assert_equal("% 1", "%% %d" % {:num => 1.0}) + assert_equal("%{num} %d", "%%{num} %%d" % {:num => 1}) + end + + test "% can be used in Ruby's own sprintf behavior" do + assert_equal "70%", "%d%%" % 70 + assert_equal "70-100%", "%d-%d%%" % [70, 100] + assert_equal "+2.30%", "%+.2f%%" % 2.3 + end + + def test_sprintf_mix_unformatted_and_formatted_named_placeholders + assert_equal("foo 1.000000", "%{name} %f" % {:name => "foo", :num => 1.0}) + end + + def test_string_interpolation_raises_an_argument_error_when_mixing_named_and_unnamed_placeholders + assert_raise(ArgumentError) { "%{name} %f" % [1.0] } + assert_raise(ArgumentError) { "%{name} %f" % [1.0, 2.0] } + end +end +