Skip to content
This repository
Browse code

Add InterpolationCompiler extension.

  • Loading branch information...
commit 91810887d1abfb28996a9183bc9004678290d28b 1 parent 0b92383
authored December 06, 2009 svenfuchs committed December 12, 2009
11  lib/i18n/backend.rb
@@ -11,5 +11,16 @@ module Backend
11 11
     autoload :Metadata,      'i18n/backend/metadata'
12 12
     autoload :Pluralization, 'i18n/backend/pluralization'
13 13
     autoload :Simple,        'i18n/backend/simple'
  14
+    autoload :ActiveRecord,          'i18n/backend/active_record'
  15
+    autoload :Base,                  'i18n/backend/base'
  16
+    autoload :Cache,                 'i18n/backend/cache'
  17
+    autoload :Chain,                 'i18n/backend/chain'
  18
+    autoload :Fallbacks,             'i18n/backend/fallbacks'
  19
+    autoload :Gettext,               'i18n/backend/gettext'
  20
+    autoload :Helpers,               'i18n/backend/helpers'
  21
+    autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
  22
+    autoload :Metadata,              'i18n/backend/metadata'
  23
+    autoload :Pluralization,         'i18n/backend/pluralization'
  24
+    autoload :Simple,                'i18n/backend/simple'
14 25
   end
15 26
 end
108  lib/i18n/backend/interpolation_compiler.rb
... ...
@@ -0,0 +1,108 @@
  1
+# encoding: utf-8
  2
+
  3
+module I18n
  4
+  module Backend
  5
+    module InterpolationCompiler
  6
+      module Compiler
  7
+        extend self
  8
+
  9
+        TOKENIZER                    = /(\\\{\{[^\}]+\}\}|\{\{[^\}]+\}\})/
  10
+        INTERPOLATION_SYNTAX_PATTERN = /(\\)?(\{\{([^\}]+)\}\})/
  11
+
  12
+        def compile_if_an_interpolation(string)
  13
+          if interpolated_str?(string)
  14
+            string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
  15
+              def i18n_interpolate(v = {})
  16
+                "#{compiled_interpolation_body(string)}"
  17
+              end
  18
+            RUBY_EVAL
  19
+          end
  20
+
  21
+          string
  22
+        end
  23
+
  24
+        def interpolated_str?(str)
  25
+          str.kind_of?(String) && str =~ INTERPOLATION_SYNTAX_PATTERN
  26
+        end
  27
+
  28
+        protected
  29
+        # tokenize("foo {{bar}} baz \\{{buz}}") # => ["foo ", "{{bar}}", " baz ", "\\{{buz}}"]
  30
+        def tokenize(str)
  31
+          str.split(TOKENIZER)
  32
+        end
  33
+
  34
+        def compiled_interpolation_body(str)
  35
+          tokenize(str).map do |token|
  36
+            (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
  37
+          end.join
  38
+        end
  39
+
  40
+        def handle_interpolation_token(interpolation, matchdata)
  41
+          escaped, pattern, key = matchdata.values_at(1, 2, 3)
  42
+          escaped ? pattern : compile_interpolation_token(key.to_sym)
  43
+        end
  44
+
  45
+        def compile_interpolation_token(key)
  46
+          "\#{#{interpolate_or_raise_missing(key)}}"
  47
+        end
  48
+
  49
+        def interpolate_or_raise_missing(key)
  50
+          escaped_key = escape_key_sym(key)
  51
+          Base::RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
  52
+        end
  53
+
  54
+        def interpolate_key(key)
  55
+          [direct_key(key), nil_key(key), missing_key(key)].join('||')
  56
+        end
  57
+
  58
+        def direct_key(key)
  59
+          "((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
  60
+        end
  61
+
  62
+        def nil_key(key)
  63
+          "(v.has_key?(#{key}) && '')"
  64
+        end
  65
+
  66
+        def missing_key(key)
  67
+          "raise(MissingInterpolationArgument.new(#{key}, self))"
  68
+        end
  69
+
  70
+        def reserved_key(key)
  71
+          "raise(ReservedInterpolationKey.new(#{key}, self))"
  72
+        end
  73
+
  74
+        def escape_plain_str(str)
  75
+          str.gsub(/"|\\|#/) {|x| "\\#{x}"}
  76
+        end
  77
+
  78
+        def escape_key_sym(key)
  79
+          # rely on Ruby to do all the hard work :)
  80
+          key.to_sym.inspect
  81
+        end
  82
+      end
  83
+      
  84
+      def interpolate(locale, string, values)
  85
+        if string.respond_to?(:i18n_interpolate)
  86
+          string.i18n_interpolate(values)
  87
+        elsif values
  88
+          super
  89
+        else
  90
+          string
  91
+        end
  92
+      end
  93
+      
  94
+      def merge_translations(locale, data)
  95
+        compile_all_strings_in(data)
  96
+        super
  97
+      end
  98
+      
  99
+      protected
  100
+      def compile_all_strings_in(data)
  101
+        data.each_value do |value|
  102
+          Compiler.compile_if_an_interpolation(value)
  103
+          compile_all_strings_in(value) if value.kind_of?(Hash)
  104
+        end
  105
+      end      
  106
+    end
  107
+  end
  108
+end
107  test/cases/backend/interpolation_compiler_test.rb
... ...
@@ -0,0 +1,107 @@
  1
+# encoding: utf-8
  2
+
  3
+require File.expand_path(File.dirname(__FILE__) + '/../../test_helper')
  4
+
  5
+class InterpolationCompilerTest < Test::Unit::TestCase
  6
+  Compiler = I18n::Backend::InterpolationCompiler::Compiler
  7
+
  8
+  def compile_and_interpolate(str, values = {})
  9
+    Compiler.compile_if_an_interpolation(str).i18n_interpolate(values)
  10
+  end
  11
+
  12
+  def assert_escapes_interpolation_key(expected, malicious_str)
  13
+    assert_equal(expected, Compiler.send(:escape_key_sym, malicious_str))
  14
+  end
  15
+
  16
+  def test_escape_key_properly_escapes
  17
+    assert_escapes_interpolation_key ':"\""',       '"'
  18
+    assert_escapes_interpolation_key ':"\\\\"',     '\\'
  19
+    assert_escapes_interpolation_key ':"\\\\\""',   '\\"'
  20
+    assert_escapes_interpolation_key ':"\#{}"',     '#{}'
  21
+    assert_escapes_interpolation_key ':"\\\\\#{}"', '\#{}'
  22
+  end
  23
+
  24
+  def assert_escapes_plain_string(expected, plain_str)
  25
+    assert_equal expected, Compiler.send(:escape_plain_str, plain_str)
  26
+  end
  27
+
  28
+  def test_escape_plain_string_properly_escapes
  29
+    assert_escapes_plain_string '\\"',    '"'
  30
+    assert_escapes_plain_string '\'',     '\''
  31
+    assert_escapes_plain_string '\\#',    '#'
  32
+    assert_escapes_plain_string '\\#{}',  '#{}'
  33
+    assert_escapes_plain_string '\\\\\\"','\\"'
  34
+  end
  35
+
  36
+  def test_non_interpolated_strings_or_arrays_dont_get_compiled
  37
+    ['abc', '\\{a}}', '{a}}', []].each do |obj|
  38
+      Compiler.compile_if_an_interpolation(obj)
  39
+      assert_equal false, obj.respond_to?(:i18n_interpolate)
  40
+    end
  41
+  end
  42
+
  43
+  def test_interpolated_string_gets_compiled
  44
+    assert_equal '-A-', compile_and_interpolate('-{{a}}-', :a => 'A')
  45
+  end
  46
+
  47
+  def assert_handles_key(str, key)
  48
+    assert_equal 'A', compile_and_interpolate(str, key => 'A')
  49
+  end
  50
+
  51
+  def test_compiles_fancy_keys
  52
+    assert_handles_key('{{\}}',      :'\\'    )
  53
+    assert_handles_key('{{#}}',      :'#'     )
  54
+    assert_handles_key('{{#{}}',     :'#{'    )
  55
+    assert_handles_key('{{#$SAFE}}', :'#$SAFE')
  56
+    assert_handles_key('{{\000}}',   :'\000'  )
  57
+    assert_handles_key('{{\'}}',     :'\''    )
  58
+    assert_handles_key('{{\'\'}}',   :'\'\''  )
  59
+    assert_handles_key('{{a.b}}',    :'a.b'   )
  60
+    assert_handles_key('{{ }}',      :' '     )
  61
+    assert_handles_key('{{:}}',      :':'     )
  62
+    assert_handles_key("{{:''}}",    :":''"   )
  63
+    assert_handles_key('{{:"}}',     :':"'    )
  64
+  end
  65
+
  66
+  def test_str_containing_only_escaped_interpolation_is_handled_correctly
  67
+    assert_equal 'abc {{x}}', compile_and_interpolate('abc \\{{x}}')
  68
+  end
  69
+
  70
+  def test_handles_weired_strings
  71
+    assert_equal '#{} a',         compile_and_interpolate('#{} {{a}}',        :a    => 'a')
  72
+    assert_equal '"#{abc}"',      compile_and_interpolate('"#{ab{{a}}c}"',    :a    => '' )
  73
+    assert_equal 'a}',            compile_and_interpolate('{{{a}}}',          :'{a' => 'a')
  74
+    assert_equal '"',             compile_and_interpolate('"{{a}}',           :a    => '' )
  75
+    assert_equal 'a{{a}}',        compile_and_interpolate('{{a}}\\{{a}}',     :a    => 'a')
  76
+    assert_equal '\\{{a}}',       compile_and_interpolate('\\\\{{a}}')
  77
+    assert_equal '\";eval("a")',  compile_and_interpolate('\";eval("{{a}}")', :a    => 'a')
  78
+    assert_equal '\";eval("a")',  compile_and_interpolate('\";eval("a"){{a}}',:a    => '' )
  79
+    assert_equal "\na",           compile_and_interpolate("\n{{a}}",          :a    => 'a')
  80
+  end
  81
+end
  82
+
  83
+class I18nBackendInterpolationCompilerTest < Test::Unit::TestCase
  84
+  class Backend
  85
+    include I18n::Backend::Base
  86
+    include I18n::Backend::InterpolationCompiler
  87
+  end
  88
+  
  89
+  include Tests::Api::Interpolation
  90
+
  91
+  def setup
  92
+    I18n.backend = Backend.new
  93
+    super
  94
+  end
  95
+  
  96
+  # pre-compile default strings to make sure we are testing I18n::Backend::InterpolationCompiler
  97
+  def interpolate(*args)
  98
+    options = args.last.kind_of?(Hash) ? args.last : {}
  99
+    if default_str = options[:default]
  100
+      I18n::Backend::InterpolationCompiler::Compiler.compile_if_an_interpolation(default_str)
  101
+    end
  102
+    super
  103
+  end
  104
+  
  105
+  # I kinda don't think this really is a correct behavior
  106
+  undef :'test interpolation: given no values it does not alter the string'
  107
+end

0 notes on commit 9181088

Please sign in to comment.
Something went wrong with that request. Please try again.