From 93dbfb621329c1eb544516533146b4f43d9360de Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Thu, 18 Nov 2010 01:06:21 +0100 Subject: [PATCH] un-monkey-patch and refactor string interpolation --- Gemfile.lock | 16 --- lib/i18n.rb | 12 +-- lib/i18n/backend/base.rb | 37 +------ lib/i18n/backend/interpolation_compiler.rb | 2 +- lib/i18n/backend/metadata.rb | 2 +- lib/i18n/core_ext/string/interpolate.rb | 96 ------------------ lib/i18n/exceptions.rb | 6 -- lib/i18n/interpolate/ruby.rb | 31 ++++++ test/core_ext/string/interpolate_test.rb | 98 ------------------- .../exceptions_test.rb} | 0 test/i18n/interpolate_test.rb | 60 ++++++++++++ .../load_path_test.rb} | 0 12 files changed, 100 insertions(+), 260 deletions(-) create mode 100644 lib/i18n/interpolate/ruby.rb delete mode 100644 test/core_ext/string/interpolate_test.rb rename test/{i18n_exceptions_test.rb => i18n/exceptions_test.rb} (100%) create mode 100644 test/i18n/interpolate_test.rb rename test/{i18n_load_path_test.rb => i18n/load_path_test.rb} (100%) diff --git a/Gemfile.lock b/Gemfile.lock index f9503f66..78fd5b84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,26 +1,13 @@ GEM remote: http://rubygems.org/ specs: - ParseTree (3.0.6) - RubyInline (>= 3.7.0) - sexp_processor (>= 3.0.0) - RubyInline (3.8.6) - ZenTest (~> 4.3) - ZenTest (4.4.0) activesupport (3.0.3) ffi (0.6.3) rake (>= 0.8.7) mocha (0.9.9) rake rake (0.8.7) - ruby-cldr (0.0.1) - ruby2ruby (1.2.5) - ruby_parser (~> 2.0) - sexp_processor (~> 3.0) - ruby_parser (2.0.5) - sexp_processor (~> 3.0) rufus-tokyo (1.0.7) - sexp_processor (3.0.5) sqlite3-ruby (1.3.2) test_declarative (0.0.4) @@ -28,12 +15,9 @@ PLATFORMS ruby DEPENDENCIES - ParseTree activesupport (~> 3.0.0) ffi mocha - ruby-cldr - ruby2ruby rufus-tokyo sqlite3-ruby test_declarative diff --git a/lib/i18n.rb b/lib/i18n.rb index d69e71bd..f063efb6 100755 --- a/lib/i18n.rb +++ b/lib/i18n.rb @@ -1,13 +1,6 @@ -# Authors:: Sven Fuchs (http://www.artweb-design.de), -# Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey), -# Stephan Soller (http://www.arkanis-development.de/), -# Saimon Moore (http://saimonmoore.net), -# Matt Aimonetti (http://railsontherun.com/) -# Copyright:: Copyright (c) 2008 The Ruby i18n Team -# License:: MIT require 'i18n/version' require 'i18n/exceptions' -require 'i18n/core_ext/string/interpolate' +require 'i18n/interpolate/ruby.rb' module I18n autoload :Backend, 'i18n/backend' @@ -16,6 +9,9 @@ module I18n autoload :Locale, 'i18n/locale' autoload :Tests, 'i18n/tests' + RESERVED_KEYS = [:scope, :default, :separator, :resolve, :object, :fallback, :format, :cascade, :raise, :rescue_format] + RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/ + class << self # Gets I18n configuration object. def config diff --git a/lib/i18n/backend/base.rb b/lib/i18n/backend/base.rb index c0faa47e..7bfdffcf 100644 --- a/lib/i18n/backend/base.rb +++ b/lib/i18n/backend/base.rb @@ -7,9 +7,6 @@ module Backend module Base include I18n::Backend::Transliterator - RESERVED_KEYS = [:scope, :default, :separator, :resolve, :object, :fallback] - RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/ - # Accepts a list of paths to translation files. Loads translations from # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml # for details. @@ -141,36 +138,14 @@ def pluralize(locale, entry, count) # # interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X' # # => "file test.txt opened by %{user}" - # - # Note that you have to double escape the \\ when you want to escape - # the {{...}} key in a string (once for the string and once for the - # interpolation). def interpolate(locale, string, values = {}) - return string unless string.is_a?(::String) && !values.empty? - - values.each do |key, value| - value = value.call(values) if interpolate_lambda?(value, string, key) - value = value.to_s unless value.is_a?(::String) - values[key] = value - end - - suppress_warnings do - string % values - end - rescue KeyError => e - if string =~ RESERVED_KEYS_PATTERN - raise ReservedInterpolationKey.new($1.to_sym, string) + if string.is_a?(::String) && !values.empty? + I18n.interpolate(string, values) else - raise MissingInterpolationArgument.new(values, string) + string end end - # returns true when the given value responds to :call and the key is - # an interpolation placeholder in the given string - def interpolate_lambda?(object, string, key) - object.respond_to?(:call) && string =~ /%\{#{key}\}|%\<#{key}>.*?\d*\.?\d*[bBdiouxXeEfgGcps]\}/ - end - # Loads a single translations file by delegating to #load_rb or # #load_yml depending on the file extension and directly merges the # data to the existing translations. Raises I18n::UnknownFileType @@ -194,12 +169,6 @@ def load_rb(filename) def load_yml(filename) YAML.load_file(filename) end - - def warn_syntax_deprecation!(locale, string) #:nodoc: - return if @skip_syntax_deprecation - warn "The {{key}} interpolation syntax in I18n messages is deprecated. Please use %{key} instead.\n#{locale} - #{string}\n" - @skip_syntax_deprecation = true - end end end end diff --git a/lib/i18n/backend/interpolation_compiler.rb b/lib/i18n/backend/interpolation_compiler.rb index 868f38dc..37f65559 100644 --- a/lib/i18n/backend/interpolation_compiler.rb +++ b/lib/i18n/backend/interpolation_compiler.rb @@ -61,7 +61,7 @@ def compile_interpolation_token(key) def interpolate_or_raise_missing(key) escaped_key = escape_key_sym(key) - Base::RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key) + RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key) end def interpolate_key(key) diff --git a/lib/i18n/backend/metadata.rb b/lib/i18n/backend/metadata.rb index 5237b4d9..eef4f70d 100644 --- a/lib/i18n/backend/metadata.rb +++ b/lib/i18n/backend/metadata.rb @@ -38,7 +38,7 @@ def translate(locale, key, options = {}) :scope => options[:scope], :default => options[:default], :separator => options[:separator], - :values => options.reject { |name, value| Base::RESERVED_KEYS.include?(name) } + :values => options.reject { |name, value| RESERVED_KEYS.include?(name) } } with_metadata(metadata) { super } end diff --git a/lib/i18n/core_ext/string/interpolate.rb b/lib/i18n/core_ext/string/interpolate.rb index 24a6fd9a..e69de29b 100644 --- a/lib/i18n/core_ext/string/interpolate.rb +++ b/lib/i18n/core_ext/string/interpolate.rb @@ -1,96 +0,0 @@ -=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/lib/i18n/exceptions.rb b/lib/i18n/exceptions.rb index 0659d9fa..6b02b760 100644 --- a/lib/i18n/exceptions.rb +++ b/lib/i18n/exceptions.rb @@ -1,9 +1,3 @@ -class KeyError < IndexError - def initialize(message = nil) - super(message || "key not found") - end -end unless defined?(KeyError) - module I18n # Handles exceptions raised in the backend. All exceptions except for # MissingTranslationData exceptions are re-raised. When a MissingTranslationData diff --git a/lib/i18n/interpolate/ruby.rb b/lib/i18n/interpolate/ruby.rb new file mode 100644 index 00000000..29b28146 --- /dev/null +++ b/lib/i18n/interpolate/ruby.rb @@ -0,0 +1,31 @@ +# heavily based on Masao Mutoh's gettext String interpolation extension +# http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb + +module I18n + INTERPOLATION_PATTERN = Regexp.union( + /%%/, + /%\{(\w+)\}/, # matches placeholders like "%{foo}" + /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%.d" + ) + + class << self + def interpolate(string, values) + raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ RESERVED_KEYS_PATTERN + raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash) + interpolate_hash(string, values) + end + + def interpolate_hash(string, values) + string.gsub(INTERPOLATION_PATTERN) do |match| + if match == '%%' + '%' + else + key = ($1 || $2).to_sym + value = values.key?(key) ? values[key] : raise(MissingInterpolationArgument.new(values, string)) + value = value.call(values) if value.respond_to?(:call) + $3 ? sprintf("%#{$3}", value) : value + end + end + end + end +end diff --git a/test/core_ext/string/interpolate_test.rb b/test/core_ext/string/interpolate_test.rb deleted file mode 100644 index 967a9838..00000000 --- a/test/core_ext/string/interpolate_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -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 diff --git a/test/i18n_exceptions_test.rb b/test/i18n/exceptions_test.rb similarity index 100% rename from test/i18n_exceptions_test.rb rename to test/i18n/exceptions_test.rb diff --git a/test/i18n/interpolate_test.rb b/test/i18n/interpolate_test.rb new file mode 100644 index 00000000..a25c44b7 --- /dev/null +++ b/test/i18n/interpolate_test.rb @@ -0,0 +1,60 @@ +require 'test_helper' + +# thanks to Masao's String extensions, some tests taken from Masao's tests +# http://github.com/mutoh/gettext/blob/edbbe1fa8238fa12c7f26f2418403015f0270e47/test/test_string.rb + +class I18nInterpolateTest < Test::Unit::TestCase + test "String interpolates a hash argument w/ named placeholders" do + assert_equal "Masao Mutoh", I18n.interpolate("%{first} %{last}", :first => 'Masao', :last => 'Mutoh' ) + end + + test "String interpolates a hash argument w/ named placeholders (reverse order)" do + assert_equal "Mutoh, Masao", I18n.interpolate("%{last}, %{first}", :first => 'Masao', :last => 'Mutoh' ) + end + + test "String interpolates named placeholders with sprintf syntax" do + assert_equal "10, 43.4", I18n.interpolate("%d, %.1f", :integer => 10, :float => 43.4) + end + + test "String interpolates named placeholders with sprintf syntax, does not recurse" do + assert_equal "%s", I18n.interpolate("%{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", I18n.interpolate("aaa", :num => 1) + end + + test "String interpolation sprintf behaviour equals Ruby 1.9 behaviour" do + assert_equal "1", I18n.interpolate("%d", :num => 1) + assert_equal "0b1", I18n.interpolate("%#b", :num => 1) + assert_equal "foo", I18n.interpolate("%s", :msg => "foo") + assert_equal "1.000000", I18n.interpolate("%f", :num => 1.0) + assert_equal " 1", I18n.interpolate("%3.0f", :num => 1.0) + assert_equal "100.00", I18n.interpolate("%2.2f", :num => 100.0) + assert_equal "0x64", I18n.interpolate("%#x", :num => 100.0) + assert_raise(ArgumentError) { I18n.interpolate("%,d", :num => 100) } + assert_raise(ArgumentError) { I18n.interpolate("%/d", :num => 100) } + end + + test "String interpolation raises an I18n::MissingInterpolationArgument when the string has extra placeholders" do + assert_raise(I18n::MissingInterpolationArgument) do # Ruby 1.9 msg: "key not found" + I18n.interpolate("%{first} %{last}", :first => 'Masao') + end + end + + test "String interpolation does not raise when extra values were passed" do + assert_nothing_raised do + assert_equal "Masao Mutoh", I18n.interpolate("%{first} %{last}", :first => 'Masao', :last => 'Mutoh', :salutation => 'Mr.' ) + end + end + + test "% acts as escape character in String interpolation" do + assert_equal "%{first}", I18n.interpolate("%%{first}", :first => 'Masao') + assert_equal "% 1", I18n.interpolate("%% %d", :num => 1.0) + assert_equal "%{num} %d", I18n.interpolate("%%{num} %%d", :num => 1) + end + + def test_sprintf_mix_unformatted_and_formatted_named_placeholders + assert_equal "foo 1.000000", I18n.interpolate("%{name} %f", :name => "foo", :num => 1.0) + end +end diff --git a/test/i18n_load_path_test.rb b/test/i18n/load_path_test.rb similarity index 100% rename from test/i18n_load_path_test.rb rename to test/i18n/load_path_test.rb