Permalink
Browse files

Add InterpolationCompiler extension.

  • Loading branch information...
thedarkone authored and Sven Fuchs committed Dec 6, 2009
1 parent 0b92383 commit 91810887d1abfb28996a9183bc9004678290d28b
Showing with 226 additions and 0 deletions.
  1. +11 −0 lib/i18n/backend.rb
  2. +108 −0 lib/i18n/backend/interpolation_compiler.rb
  3. +107 −0 test/cases/backend/interpolation_compiler_test.rb
View
@@ -11,5 +11,16 @@ module Backend
autoload :Metadata, 'i18n/backend/metadata'
autoload :Pluralization, 'i18n/backend/pluralization'
autoload :Simple, 'i18n/backend/simple'
+ autoload :ActiveRecord, 'i18n/backend/active_record'
+ autoload :Base, 'i18n/backend/base'
+ autoload :Cache, 'i18n/backend/cache'
+ autoload :Chain, 'i18n/backend/chain'
+ autoload :Fallbacks, 'i18n/backend/fallbacks'
+ autoload :Gettext, 'i18n/backend/gettext'
+ autoload :Helpers, 'i18n/backend/helpers'
+ autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
+ autoload :Metadata, 'i18n/backend/metadata'
+ autoload :Pluralization, 'i18n/backend/pluralization'
+ autoload :Simple, 'i18n/backend/simple'
end
end
@@ -0,0 +1,108 @@
+# encoding: utf-8
+
+module I18n
+ module Backend
+ module InterpolationCompiler
+ module Compiler
+ extend self
+
+ TOKENIZER = /(\\\{\{[^\}]+\}\}|\{\{[^\}]+\}\})/
+ INTERPOLATION_SYNTAX_PATTERN = /(\\)?(\{\{([^\}]+)\}\})/
+
+ def compile_if_an_interpolation(string)
+ if interpolated_str?(string)
+ string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
+ def i18n_interpolate(v = {})
+ "#{compiled_interpolation_body(string)}"
+ end
+ RUBY_EVAL
+ end
+
+ string
+ end
+
+ def interpolated_str?(str)
+ str.kind_of?(String) && str =~ INTERPOLATION_SYNTAX_PATTERN
+ end
+
+ protected
+ # tokenize("foo {{bar}} baz \\{{buz}}") # => ["foo ", "{{bar}}", " baz ", "\\{{buz}}"]
+ def tokenize(str)
+ str.split(TOKENIZER)
+ end
+
+ def compiled_interpolation_body(str)
+ tokenize(str).map do |token|
+ (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
+ end.join
+ end
+
+ def handle_interpolation_token(interpolation, matchdata)
+ escaped, pattern, key = matchdata.values_at(1, 2, 3)
+ escaped ? pattern : compile_interpolation_token(key.to_sym)
+ end
+
+ def compile_interpolation_token(key)
+ "\#{#{interpolate_or_raise_missing(key)}}"
+ end
+
+ 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)
+ end
+
+ def interpolate_key(key)
+ [direct_key(key), nil_key(key), missing_key(key)].join('||')
+ end
+
+ def direct_key(key)
+ "((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
+ end
+
+ def nil_key(key)
+ "(v.has_key?(#{key}) && '')"
+ end
+
+ def missing_key(key)
+ "raise(MissingInterpolationArgument.new(#{key}, self))"
+ end
+
+ def reserved_key(key)
+ "raise(ReservedInterpolationKey.new(#{key}, self))"
+ end
+
+ def escape_plain_str(str)
+ str.gsub(/"|\\|#/) {|x| "\\#{x}"}
+ end
+
+ def escape_key_sym(key)
+ # rely on Ruby to do all the hard work :)
+ key.to_sym.inspect
+ end
+ end
+
+ def interpolate(locale, string, values)
+ if string.respond_to?(:i18n_interpolate)
+ string.i18n_interpolate(values)
+ elsif values
+ super
+ else
+ string
+ end
+ end
+
+ def merge_translations(locale, data)
+ compile_all_strings_in(data)
+ super
+ end
+
+ protected
+ def compile_all_strings_in(data)
+ data.each_value do |value|
+ Compiler.compile_if_an_interpolation(value)
+ compile_all_strings_in(value) if value.kind_of?(Hash)
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,107 @@
+# encoding: utf-8
+
+require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
+
+class InterpolationCompilerTest < Test::Unit::TestCase
+ Compiler = I18n::Backend::InterpolationCompiler::Compiler
+
+ def compile_and_interpolate(str, values = {})
+ Compiler.compile_if_an_interpolation(str).i18n_interpolate(values)
+ end
+
+ def assert_escapes_interpolation_key(expected, malicious_str)
+ assert_equal(expected, Compiler.send(:escape_key_sym, malicious_str))
+ end
+
+ def test_escape_key_properly_escapes
+ assert_escapes_interpolation_key ':"\""', '"'
+ assert_escapes_interpolation_key ':"\\\\"', '\\'
+ assert_escapes_interpolation_key ':"\\\\\""', '\\"'
+ assert_escapes_interpolation_key ':"\#{}"', '#{}'
+ assert_escapes_interpolation_key ':"\\\\\#{}"', '\#{}'
+ end
+
+ def assert_escapes_plain_string(expected, plain_str)
+ assert_equal expected, Compiler.send(:escape_plain_str, plain_str)
+ end
+
+ def test_escape_plain_string_properly_escapes
+ assert_escapes_plain_string '\\"', '"'
+ assert_escapes_plain_string '\'', '\''
+ assert_escapes_plain_string '\\#', '#'
+ assert_escapes_plain_string '\\#{}', '#{}'
+ assert_escapes_plain_string '\\\\\\"','\\"'
+ end
+
+ def test_non_interpolated_strings_or_arrays_dont_get_compiled
+ ['abc', '\\{a}}', '{a}}', []].each do |obj|
+ Compiler.compile_if_an_interpolation(obj)
+ assert_equal false, obj.respond_to?(:i18n_interpolate)
+ end
+ end
+
+ def test_interpolated_string_gets_compiled
+ assert_equal '-A-', compile_and_interpolate('-{{a}}-', :a => 'A')
+ end
+
+ def assert_handles_key(str, key)
+ assert_equal 'A', compile_and_interpolate(str, key => 'A')
+ end
+
+ def test_compiles_fancy_keys
+ assert_handles_key('{{\}}', :'\\' )
+ assert_handles_key('{{#}}', :'#' )
+ assert_handles_key('{{#{}}', :'#{' )
+ assert_handles_key('{{#$SAFE}}', :'#$SAFE')
+ assert_handles_key('{{\000}}', :'\000' )
+ assert_handles_key('{{\'}}', :'\'' )
+ assert_handles_key('{{\'\'}}', :'\'\'' )
+ assert_handles_key('{{a.b}}', :'a.b' )
+ assert_handles_key('{{ }}', :' ' )
+ assert_handles_key('{{:}}', :':' )
+ assert_handles_key("{{:''}}", :":''" )
+ assert_handles_key('{{:"}}', :':"' )
+ end
+
+ def test_str_containing_only_escaped_interpolation_is_handled_correctly
+ assert_equal 'abc {{x}}', compile_and_interpolate('abc \\{{x}}')
+ end
+
+ def test_handles_weired_strings
+ assert_equal '#{} a', compile_and_interpolate('#{} {{a}}', :a => 'a')
+ assert_equal '"#{abc}"', compile_and_interpolate('"#{ab{{a}}c}"', :a => '' )
+ assert_equal 'a}', compile_and_interpolate('{{{a}}}', :'{a' => 'a')
+ assert_equal '"', compile_and_interpolate('"{{a}}', :a => '' )
+ assert_equal 'a{{a}}', compile_and_interpolate('{{a}}\\{{a}}', :a => 'a')
+ assert_equal '\\{{a}}', compile_and_interpolate('\\\\{{a}}')
+ assert_equal '\";eval("a")', compile_and_interpolate('\";eval("{{a}}")', :a => 'a')
+ assert_equal '\";eval("a")', compile_and_interpolate('\";eval("a"){{a}}',:a => '' )
+ assert_equal "\na", compile_and_interpolate("\n{{a}}", :a => 'a')
+ end
+end
+
+class I18nBackendInterpolationCompilerTest < Test::Unit::TestCase
+ class Backend
+ include I18n::Backend::Base
+ include I18n::Backend::InterpolationCompiler
+ end
+
+ include Tests::Api::Interpolation
+
+ def setup
+ I18n.backend = Backend.new
+ super
+ end
+
+ # pre-compile default strings to make sure we are testing I18n::Backend::InterpolationCompiler
+ def interpolate(*args)
+ options = args.last.kind_of?(Hash) ? args.last : {}
+ if default_str = options[:default]
+ I18n::Backend::InterpolationCompiler::Compiler.compile_if_an_interpolation(default_str)
+ end
+ super
+ end
+
+ # I kinda don't think this really is a correct behavior
+ undef :'test interpolation: given no values it does not alter the string'
+end

0 comments on commit 9181088

Please sign in to comment.