i18nliner 0.0.1 → 0.0.2

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.
Files changed (37) hide show
  1. data/README.md +151 -104
  2. data/lib/i18nliner/base.rb +47 -0
  3. data/lib/i18nliner/call_helpers.rb +39 -13
  4. data/lib/i18nliner/commands/check.rb +20 -10
  5. data/lib/i18nliner/commands/dump.rb +17 -0
  6. data/lib/i18nliner/commands/generic_command.rb +10 -1
  7. data/lib/i18nliner/errors.rb +11 -1
  8. data/lib/i18nliner/erubis.rb +12 -0
  9. data/lib/i18nliner/extensions/controller.rb +25 -0
  10. data/lib/i18nliner/extensions/core.rb +61 -0
  11. data/lib/i18nliner/extensions/inferpolation.rb +28 -0
  12. data/lib/i18nliner/extensions/model.rb +29 -0
  13. data/lib/i18nliner/extensions/view.rb +28 -0
  14. data/lib/i18nliner/extractors/ruby_extractor.rb +15 -33
  15. data/lib/i18nliner/extractors/sexp_helper.rb +38 -0
  16. data/lib/i18nliner/extractors/translate_call.rb +9 -11
  17. data/lib/i18nliner/extractors/translation_hash.rb +6 -6
  18. data/lib/i18nliner/pre_processors/erb_pre_processor.rb +334 -0
  19. data/lib/i18nliner/processors/abstract_processor.rb +26 -5
  20. data/lib/i18nliner/processors/erb_processor.rb +19 -3
  21. data/lib/i18nliner/processors/ruby_processor.rb +16 -5
  22. data/lib/i18nliner/railtie.rb +27 -0
  23. data/lib/i18nliner/scope.rb +4 -0
  24. data/lib/i18nliner.rb +5 -29
  25. data/lib/tasks/i18nliner.rake +10 -4
  26. data/spec/commands/check_spec.rb +22 -0
  27. data/spec/commands/dump_spec.rb +27 -0
  28. data/spec/extensions/core_spec.rb +71 -0
  29. data/spec/extensions/inferpolation_spec.rb +41 -0
  30. data/spec/extensions/view_spec.rb +42 -0
  31. data/spec/extractors/ruby_extractor_spec.rb +4 -5
  32. data/spec/extractors/translate_call_spec.rb +29 -2
  33. data/spec/fixtures/app/models/invalid.rb +5 -0
  34. data/spec/fixtures/app/models/valid.rb +5 -0
  35. data/spec/pre_processors/erb_pre_processor_spec.rb +194 -0
  36. data/spec/processors/erb_processor_spec.rb +28 -0
  37. metadata +88 -5
@@ -0,0 +1,334 @@
1
+ require 'i18nliner/errors'
2
+ require 'i18nliner/call_helpers'
3
+ require 'i18nliner/extractors/sexp_helper'
4
+ require 'nokogiri'
5
+ require 'ruby_parser'
6
+ require 'ruby2ruby'
7
+
8
+ module I18nliner
9
+ module PreProcessors
10
+ class ErbPreProcessor
11
+
12
+ class Context
13
+ attr_reader :buffer, :parent
14
+
15
+ def initialize(parent = nil)
16
+ @parent = parent
17
+ @buffer = ''
18
+ end
19
+
20
+ def <<(string)
21
+ if string =~ ERB_T_BLOCK_EXPRESSION
22
+ TBlock.new(self, $&)
23
+ else
24
+ @buffer << string
25
+ self
26
+ end
27
+ end
28
+
29
+ def result
30
+ @buffer
31
+ end
32
+ end
33
+
34
+ class Helper
35
+ include Extractors::SexpHelper
36
+
37
+ DEFINITIONS = [
38
+ {:method => :link_to, :pattern => /link_to/, :arg => 0}
39
+ ]
40
+ RUBY2RUBY = Ruby2Ruby.new
41
+ PARSER = RubyParser.new
42
+
43
+ def self.match_for(string)
44
+ DEFINITIONS.each do |info|
45
+ return Helper.new(info, string) if string =~ info[:pattern]
46
+ end
47
+ nil
48
+ end
49
+
50
+ attr_reader :placeholder, :wrapper
51
+ attr_accessor :content
52
+
53
+ def initialize(info, source)
54
+ @arg = info[:arg]
55
+ @method = info[:method]
56
+ @source = source
57
+ end
58
+
59
+ SEXP_ARG_OFFSET = 3
60
+ def wrappable?
61
+ return @wrappable if !@wrappable.nil?
62
+ begin
63
+ sexps = PARSER.parse(@source)
64
+ @wrappable = sexps.sexp_type == :call &&
65
+ sexps[1].nil? &&
66
+ sexps[2] == @method &&
67
+ sexps[@arg + SEXP_ARG_OFFSET]
68
+ extract_content!(sexps) if @wrappable
69
+ @wrappable
70
+ end
71
+ end
72
+
73
+ def extract_content!(sexps)
74
+ sexp = sexps[@arg + SEXP_ARG_OFFSET]
75
+ if stringish?(sexp)
76
+ @content = string_from(sexp)
77
+ else
78
+ @placeholder = RUBY2RUBY.process(sexp)
79
+ end
80
+ sexps[@arg + SEXP_ARG_OFFSET] = Sexp.new(:str, "\\1")
81
+ @wrapper = RUBY2RUBY.process(sexps)
82
+ end
83
+ end
84
+
85
+ class TBlock < Context
86
+ include CallHelpers
87
+
88
+ def initialize(parent, content)
89
+ super(parent)
90
+ @lines = content.count("\n")
91
+ end
92
+
93
+ def <<(string)
94
+ case string
95
+ when ERB_BLOCK_EXPRESSION
96
+ if string =~ ERB_T_BLOCK_EXPRESSION
97
+ TBlock.new(self, $&)
98
+ else
99
+ raise TBlockNestingError.new("can't nest block expressions inside a t block")
100
+ end
101
+ when ERB_STATEMENT
102
+ if string =~ ERB_END_STATEMENT
103
+ @parent << result
104
+ else
105
+ raise TBlockNestingError.new("can't nest statements inside a t block")
106
+ end
107
+ else
108
+ # expressions and the like are handled a bit later
109
+ # TODO: perhaps a tad more efficient to capture/transform them
110
+ # here?
111
+ @buffer << string
112
+ self
113
+ end
114
+ end
115
+
116
+ def result
117
+ @lines += @buffer.count("\n")
118
+ key, default, options, wrappers = normalize_call
119
+ result = "<%= t :#{key}, #{default}"
120
+ result << ", " << options if options
121
+ result << ", " << wrappers if wrappers
122
+ result << (@lines > 0 ? "\n" * @lines : " ")
123
+ result << "%>"
124
+ end
125
+
126
+ # get a unique and reasonable looking key for a given erb
127
+ # expression
128
+ def infer_interpolation_key(string, others)
129
+ key = string.downcase
130
+ key.sub!(/\.html_safe\z/, '')
131
+ key.gsub!(/[^a-z0-9]/, ' ')
132
+ key.strip!
133
+ key.gsub!(/ +/, '_')
134
+ key.slice!(20)
135
+ i = 0
136
+ base_key = key
137
+ while others.key?(key) && others[key] != string
138
+ key = "#{base_key}_#{i}"
139
+ i += 1
140
+ end
141
+ key
142
+ end
143
+
144
+ def extract_wrappers!(source, wrappers, placeholder_map)
145
+ source = extract_html_wrappers!(source, wrappers, placeholder_map)
146
+ source = extract_helper_wrappers!(source, wrappers, placeholder_map)
147
+ source
148
+ end
149
+
150
+ def find_or_add_wrapper(wrapper, wrappers)
151
+ unless pos = wrappers.index(wrapper)
152
+ pos = wrappers.size
153
+ wrappers << wrapper
154
+ end
155
+ pos
156
+ end
157
+
158
+ # incidentally this converts entities to their corresponding values
159
+ def extract_html_wrappers!(source, wrappers, placeholder_map)
160
+ default = ''
161
+ nodes = Nokogiri::HTML.fragment(source).children
162
+ nodes.each do |node|
163
+ if node.is_a?(Nokogiri::XML::Text)
164
+ default << node.content
165
+ elsif text = extract_text(node)
166
+ wrapper = node.to_s.sub(text, "\\\\1")
167
+ wrapper = prepare_wrapper(wrapper, placeholder_map)
168
+ pos = find_or_add_wrapper(wrapper, wrappers)
169
+ default << wrap(text, pos + 1)
170
+ else # no wrapped text (e.g. <input>)
171
+ key = "__I18NLINER_#{placeholder_map.size}__"
172
+ placeholder_map[key] = node.to_s.inspect << ".html_safe"
173
+ default << key
174
+ end
175
+ end
176
+ default
177
+ end
178
+
179
+ def extract_helper_wrappers!(source, wrappers, placeholder_map)
180
+ source.gsub(TEMP_PLACEHOLDER) do |string|
181
+ if (helper = Helper.match_for(placeholder_map[string])) && helper.wrappable?
182
+ placeholder_map.delete(string)
183
+ if helper.placeholder # e.g. link_to(name) -> *%{name}*
184
+ helper.content = "__I18NLINER_#{placeholder_map.size}__"
185
+ placeholder_map[helper.content] = helper.placeholder
186
+ end
187
+ pos = find_or_add_wrapper(helper.wrapper, wrappers)
188
+ wrap(helper.content, pos + 1)
189
+ else
190
+ string
191
+ end
192
+ end
193
+ end
194
+
195
+ def prepare_wrapper(content, placeholder_map)
196
+ content = content.inspect
197
+ content.gsub!(TEMP_PLACEHOLDER) do |key|
198
+ "\#{#{placeholder_map[key]}}"
199
+ end
200
+ content
201
+ end
202
+
203
+ def extract_temp_placeholders!
204
+ extract_placeholders!(@buffer, ERB_EXPRESSION, false) do |str, map|
205
+ ["__I18NLINER_#{map.size}__", str]
206
+ end
207
+ end
208
+
209
+ def extract_placeholders!(buffer = @buffer, pattern = ERB_EXPRESSION, wrap_placeholder = true)
210
+ map = {}
211
+ buffer.gsub!(pattern) do |str|
212
+ key, str = yield($~[:content], map)
213
+ map[key] = str
214
+ wrap_placeholder ? "%{#{key}}" : key
215
+ end
216
+ map
217
+ end
218
+
219
+ TEMP_PLACEHOLDER = /(?<content>__I18NLINER_\d+__)/
220
+ def normalize_call
221
+ wrappers = []
222
+
223
+ temp_map = extract_temp_placeholders!
224
+ default = extract_wrappers!(@buffer, wrappers, temp_map)
225
+ options = extract_placeholders!(default, TEMP_PLACEHOLDER) do |str, map|
226
+ [infer_interpolation_key(temp_map[str], map), temp_map[str]]
227
+ end
228
+
229
+ default.strip!
230
+ default.gsub!(/\s+/, ' ')
231
+
232
+ key = infer_key(default)
233
+ default = default.inspect
234
+ options = options_to_ruby(options)
235
+ wrappers = wrappers_to_ruby(wrappers)
236
+ [key, default, options, wrappers]
237
+ end
238
+
239
+ def options_to_ruby(options)
240
+ return if options.size == 0
241
+ options.map do |key, value|
242
+ ":" << key << " => (" << value << ")"
243
+ end.join(", ")
244
+ end
245
+
246
+ def wrappers_to_ruby(wrappers)
247
+ return if wrappers.size == 0
248
+ ":wrappers => [" << wrappers.join(", ") << "]"
249
+ end
250
+
251
+ def extract_text(root_node)
252
+ text = nil
253
+ nodes = root_node.children.to_a
254
+ while node = nodes.shift
255
+ if node.is_a?(Nokogiri::XML::Text) && !node.content.strip.empty?
256
+ raise UnwrappableContentError.new "multiple text nodes in html markup" if text
257
+ text = node.content
258
+ else
259
+ nodes.concat node.children
260
+ end
261
+ end
262
+ text
263
+ end
264
+
265
+ def wrap(text, index)
266
+ delimiter = "*" * index
267
+ "" << delimiter << text << delimiter
268
+ end
269
+
270
+ def infer_wrappers(source)
271
+ wrappers = []
272
+ [source, wrappers]
273
+ end
274
+ end
275
+
276
+ # need to evaluate all expressions and statements, so we can
277
+ # correctly match the start/end of the `t` block expression
278
+ # (including nested ones)
279
+ ERB_EXPRESSION = /<%=\s*(?<content>.*?)\s*%>/
280
+ ERB_BLOCK_EXPRESSION = /
281
+ \A
282
+ <%=
283
+ .*?
284
+ (\sdo|\{)
285
+ (\s*\|[^\|]+\|)?
286
+ \s*
287
+ %>
288
+ \z
289
+ /xm
290
+ ERB_T_BLOCK_EXPRESSION = /
291
+ \A
292
+ <%=
293
+ \s*
294
+ t
295
+ \s*?
296
+ (\(\)\s*)?
297
+ (\sdo|\{)
298
+ \s*
299
+ %>
300
+ \z
301
+ /xm
302
+ ERB_STATEMENT = /\A<%[^=]/
303
+ ERB_END_STATEMENT = /
304
+ \A
305
+ <%
306
+ \s*
307
+ (end|\})
308
+ (\W|%>\z)
309
+ /xm
310
+ ERB_TOKENIZER = /(<%.*?%>)/m
311
+
312
+ def self.process(source)
313
+ new(source).result
314
+ end
315
+
316
+ def initialize(source)
317
+ @source = source
318
+ end
319
+
320
+ def result
321
+ # the basic idea:
322
+ # 1. whenever we find a t block expr, go till we find the end
323
+ # 2. if we find another t block expr before the end, goto step 1
324
+ # 3. capture any inline expressions along the way
325
+ # 4. if we find *any* other statement or block expr, abort,
326
+ # since it's a no-go
327
+ # TODO get line numbers for errors
328
+ ctx = @source.split(ERB_TOKENIZER).inject(Context.new, :<<)
329
+ raise MalformedErbError.new('possibly unterminated block expression') if ctx.parent
330
+ ctx.result
331
+ end
332
+ end
333
+ end
334
+ end
@@ -1,10 +1,19 @@
1
+ require 'globby'
2
+ require 'i18nliner/base'
3
+ require 'i18nliner/processors'
4
+
1
5
  module I18nliner
2
6
  module Processors
3
7
  class AbstractProcessor
8
+ attr_reader :translation_count, :file_count
9
+
4
10
  def initialize(translations, options = {})
5
11
  @translations = translations
12
+ @translation_count = 0
13
+ @file_count = 0
6
14
  @only = options[:only]
7
- @checker = options[:checker] || methods(:noop_checker)
15
+ @checker = options[:checker] || method(:noop_checker)
16
+ @pattern = options[:pattern] || self.class.default_pattern
8
17
  end
9
18
 
10
19
  def noop_checker(file)
@@ -13,21 +22,33 @@ module I18nliner
13
22
 
14
23
  def files
15
24
  @files ||= begin
16
- files = Globby.select(@pattern)
17
- files = files.select(@only) if @only
25
+ files = Globby.select(Array(@pattern))
26
+ files = files.select(Array(@only.dup)) if @only
18
27
  files.reject(I18nliner.ignore)
19
28
  end
20
29
  end
21
30
 
22
31
  def check_files
23
- files.each do |file|
24
- @checker.call file, &methods(:check_file)
32
+ Dir.chdir(I18nliner.base_path) do
33
+ files.each do |file|
34
+ @checker.call file, &method(:check_file)
35
+ end
25
36
  end
26
37
  end
27
38
 
39
+ def check_file(file)
40
+ @file_count += 1
41
+ check_contents(source_for(file), scope_for(file))
42
+ end
43
+
28
44
  def self.inherited(klass)
29
45
  Processors.register klass
30
46
  end
47
+
48
+ def self.default_pattern(*pattern)
49
+ @pattern ||= []
50
+ @pattern.concat(pattern)
51
+ end
31
52
  end
32
53
  end
33
54
  end
@@ -1,9 +1,25 @@
1
+ if defined?(::Rails)
2
+ require 'i18nliner/erubis'
3
+ else
4
+ require 'erubis'
5
+ end
6
+ require 'i18nliner/processors/ruby_processor'
7
+ require 'i18nliner/pre_processors/erb_pre_processor'
8
+
1
9
  module I18nliner
2
10
  module Processors
3
11
  class ErbProcessor < RubyProcessor
4
- def source_for(file)
5
- # TODO: pre-process for block fu
6
- Erubis::Eruby.new(super).src
12
+ default_pattern '*.erb'
13
+
14
+ if defined?(::Rails) # block expressions and all that jazz
15
+ def pre_process(source)
16
+ I18nliner::Erubis.new(source).src
17
+ end
18
+ else
19
+ def pre_process(source)
20
+ source = PreProcessors::ErbPreProcessor.process(source)
21
+ Erubis::Eruby.new(source).src
22
+ end
7
23
  end
8
24
 
9
25
  def scope_for(path)
@@ -1,11 +1,18 @@
1
+ require 'i18nliner/processors/abstract_processor'
2
+ require 'i18nliner/extractors/ruby_extractor'
3
+ require 'i18nliner/scope'
4
+
1
5
  module I18nliner
2
6
  module Processors
3
7
  class RubyProcessor < AbstractProcessor
4
- def check_file(file)
5
- sexps = RubyParser.new.parse(source_for(file))
6
- extractor = Extractors::RubyExtractor.new(sexps, scope_for(file))
8
+ default_pattern '*.rb'
9
+
10
+ def check_contents(source, scope = Scope.new)
11
+ sexps = RubyParser.new.parse(pre_process(source))
12
+ extractor = Extractors::RubyExtractor.new(sexps, scope)
7
13
  extractor.each_translation do |key, value|
8
- @translations.line = extractor.line
14
+ @translation_count += 1
15
+ @translations.line = extractor.current_line
9
16
  @translations[key] = value
10
17
  end
11
18
  end
@@ -15,7 +22,11 @@ module I18nliner
15
22
  end
16
23
 
17
24
  def scope_for(path)
18
- Scope.new
25
+ Scope.root
26
+ end
27
+
28
+ def pre_process(source)
29
+ source
19
30
  end
20
31
  end
21
32
  end
@@ -0,0 +1,27 @@
1
+ require 'i18nliner/extensions/controller'
2
+ require 'i18nliner/extensions/view'
3
+ require 'i18nliner/extensions/model'
4
+
5
+ module I18nliner
6
+ class Railtie < Rails::Railtie
7
+ ActiveSupport.on_load :action_controller do
8
+ ActionController::Base.send :include, I18nliner::Extensions::Controller
9
+ end
10
+
11
+ ActiveSupport.on_load :action_view do
12
+ require 'i18nliner/erubis'
13
+ ActionView::Template::Handlers::ERB.erb_implementation = I18nliner::Erubis
14
+ ActionView::Base.send :include, I18nliner::Extensions::View
15
+ end
16
+
17
+ ActiveSupport.on_load :active_record do
18
+ ActiveRecord::Base.send :include, I18nliner::Extensions::Model
19
+ end
20
+
21
+ rake_tasks do
22
+ load "tasks/i18nliner.rake"
23
+ end
24
+ end
25
+ end
26
+
27
+
@@ -20,5 +20,9 @@ module I18nliner
20
20
  key
21
21
  end
22
22
  end
23
+
24
+ def self.root
25
+ @root ||= new
26
+ end
23
27
  end
24
28
  end
data/lib/i18nliner.rb CHANGED
@@ -1,31 +1,7 @@
1
- require 'active_support/core_ext/string/inflections'
1
+ require 'i18n'
2
+ require 'i18nliner/base'
2
3
 
3
- module I18nliner
4
- def self.translations
5
- end
4
+ require 'i18nliner/extensions/core'
5
+ I18n.send :extend, I18nliner::Extensions::Core
6
6
 
7
- def self.look_up(*args)
8
- end
9
-
10
- def self.setting(key, value)
11
- instance_eval <<-CODE
12
- def #{key}(value = nil)
13
- if value && block_given?
14
- begin
15
- value_was = @#{key}
16
- @#{key} = value
17
- yield
18
- ensure
19
- @#{key} = value_was
20
- end
21
- else
22
- @#{key} = #{value.inspect} if @#{key}.nil?
23
- @#{key}
24
- end
25
- end
26
- CODE
27
- end
28
-
29
- setting :inferred_key_format, :underscored_crc32
30
- setting :infer_interpolation_values, true
31
- end
7
+ require 'i18nliner/railtie' if defined?(Rails)
@@ -1,14 +1,20 @@
1
1
  namespace :i18nliner do
2
2
  desc "Verifies all translation calls"
3
3
  task :check => :environment do
4
- options = {:only => ENV['ONLY'])}
5
- @command = I18nliner::Commands::Check.run(options) or exit 1
4
+ require 'i18nliner/commands/check'
5
+
6
+ options = {:only => ENV['ONLY']}
7
+ @command = I18nliner::Commands::Check.run(options)
8
+ @command.success? or exit 1
6
9
  end
7
10
 
8
11
  desc "Generates a new [default_locale].yml file for all translations"
9
12
  task :dump => :check do
10
- options = {:translations => @command.translations}
11
- @command = I18nliner::Commands::Dump.run(options) or exit 1
13
+ require 'i18nliner/commands/dump'
14
+
15
+ options = {:translations => @command.translations, :file => ENV['YML_FILE']}
16
+ @command = I18nliner::Commands::Dump.run(options)
17
+ @command.success? or exit 1
12
18
  end
13
19
  end
14
20
 
@@ -0,0 +1,22 @@
1
+ require 'i18nliner/commands/check'
2
+
3
+ describe I18nliner::Commands::Check do
4
+ describe ".run" do
5
+
6
+ around do |example|
7
+ I18nliner.base_path "spec/fixtures" do
8
+ example.run
9
+ end
10
+ end
11
+
12
+ it "should find errors" do
13
+ checker = I18nliner::Commands::Check.new({:silent => true})
14
+ checker.check_files
15
+ checker.translations.values.should == ["welcome, %{name}", "Hello World", "*This* is a test, %{user}"]
16
+ checker.errors.size.should == 2
17
+ checker.errors.each do |error|
18
+ error.should =~ /\Ainvalid signature/
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+ require 'i18nliner/commands/dump'
3
+ require 'tmpdir'
4
+
5
+ describe I18nliner::Commands::Dump do
6
+ describe ".run" do
7
+
8
+ around do |example|
9
+ Dir.mktmpdir do |dir|
10
+ I18nliner.base_path dir do
11
+ example.run
12
+ end
13
+ end
14
+ end
15
+
16
+ it "should dump translations in utf8" do
17
+ dumper = I18nliner::Commands::Dump.new({:silent => true, :translations => {'i18n' => "Iñtërnâtiônàlizætiøn"}})
18
+ dumper.run
19
+ File.read(dumper.yml_file).gsub(/\s+$/, '').should == <<-YML.strip_heredoc.strip
20
+ ---
21
+ #{I18n.default_locale}:
22
+ i18n: Iñtërnâtiônàlizætiøn
23
+ YML
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,71 @@
1
+ require 'i18n'
2
+ require 'i18nliner/extensions/core'
3
+
4
+ describe I18nliner::Extensions::Core do
5
+ let(:i18n) do
6
+ Module.new do
7
+ extend(Module.new do
8
+ def translate(*args)
9
+ simple_translate(args[0], args[1])
10
+ end
11
+
12
+ def simple_translate(key, options)
13
+ string = options.delete(:default)
14
+ interpolate_hash(string, options)
15
+ end
16
+
17
+ def interpolate_hash(string, values)
18
+ I18n.interpolate_hash(string, values)
19
+ end
20
+ end)
21
+ extend I18nliner::Extensions::Core
22
+ end
23
+ end
24
+
25
+ describe ".translate" do
26
+ it "should should normalize the arguments passed into the original translate" do
27
+ expect(i18n).to receive(:simple_translate).with("hello_name_84ff273f", :default => "Hello %{name}", :name => "bob")
28
+ i18n.translate("Hello %{name}", :name => "bob")
29
+ end
30
+
31
+ it "should apply wrappers" do
32
+ result = i18n.translate("Hello *bob*. Click **here**", :wrappers => ['<b>\1</b>', '<a href="/">\1</a>'])
33
+ result.should == "Hello <b>bob</b>. Click <a href=\"/\">here</a>"
34
+ result.should be_html_safe
35
+ end
36
+
37
+ it "should html-escape the default when applying wrappers" do
38
+ i18n.translate("*bacon* > narwhals", :wrappers => ['<b>\1</b>']).
39
+ should == "<b>bacon</b> &gt; narwhals"
40
+ end
41
+ end
42
+
43
+ describe ".translate!" do
44
+ it "should behave like translate" do
45
+ expect(i18n).to receive(:simple_translate).with("hello_name_84ff273f", :default => "Hello %{name}", :name => "bob")
46
+ i18n.translate!("Hello %{name}", :name => "bob")
47
+ end
48
+ end
49
+
50
+ describe ".interpolate_hash" do
51
+ it "should not mark the result as html-safe if none of the components are html-safe" do
52
+ result = i18n.interpolate_hash("hello %{name}", :name => "<script>")
53
+ result.should == "hello <script>"
54
+ result.should_not be_html_safe
55
+ end
56
+
57
+ it "should html-escape values if the string is html-safe" do
58
+ result = i18n.interpolate_hash("some markup: %{markup}".html_safe, :markup => "<html>")
59
+ result.should == "some markup: &lt;html&gt;"
60
+ result.should be_html_safe
61
+ end
62
+
63
+ it "should html-escape the string and other values if any value is html-safe" do
64
+ markup = "<input>"
65
+ result = i18n.interpolate_hash("type %{input} & you get this: %{output}", :input => markup, :output => markup.html_safe)
66
+ result.should == "type &lt;input&gt; &amp; you get this: <input>"
67
+ result.should be_html_safe
68
+ end
69
+ end
70
+ end
71
+