i18nliner 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+