actionview 6.0.3.3 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +177 -208
  3. data/MIT-LICENSE +1 -1
  4. data/lib/action_view.rb +4 -1
  5. data/lib/action_view/base.rb +19 -50
  6. data/lib/action_view/cache_expiry.rb +1 -2
  7. data/lib/action_view/dependency_tracker.rb +10 -4
  8. data/lib/action_view/digestor.rb +3 -2
  9. data/lib/action_view/gem_version.rb +3 -3
  10. data/lib/action_view/helpers/asset_tag_helper.rb +55 -15
  11. data/lib/action_view/helpers/asset_url_helper.rb +6 -4
  12. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  13. data/lib/action_view/helpers/cache_helper.rb +10 -16
  14. data/lib/action_view/helpers/date_helper.rb +4 -4
  15. data/lib/action_view/helpers/form_helper.rb +66 -30
  16. data/lib/action_view/helpers/form_options_helper.rb +7 -16
  17. data/lib/action_view/helpers/form_tag_helper.rb +7 -7
  18. data/lib/action_view/helpers/javascript_helper.rb +3 -3
  19. data/lib/action_view/helpers/number_helper.rb +6 -6
  20. data/lib/action_view/helpers/rendering_helper.rb +11 -3
  21. data/lib/action_view/helpers/sanitize_helper.rb +2 -2
  22. data/lib/action_view/helpers/tag_helper.rb +92 -17
  23. data/lib/action_view/helpers/tags/base.rb +9 -5
  24. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  25. data/lib/action_view/helpers/tags/date_select.rb +2 -2
  26. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -1
  27. data/lib/action_view/helpers/tags/label.rb +4 -0
  28. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  29. data/lib/action_view/helpers/tags/select.rb +1 -1
  30. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  31. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  32. data/lib/action_view/helpers/text_helper.rb +1 -1
  33. data/lib/action_view/helpers/translation_helper.rb +87 -51
  34. data/lib/action_view/helpers/url_helper.rb +107 -13
  35. data/lib/action_view/layouts.rb +3 -2
  36. data/lib/action_view/log_subscriber.rb +26 -10
  37. data/lib/action_view/lookup_context.rb +3 -18
  38. data/lib/action_view/path_set.rb +0 -3
  39. data/lib/action_view/railtie.rb +39 -46
  40. data/lib/action_view/renderer/abstract_renderer.rb +93 -14
  41. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  42. data/lib/action_view/renderer/object_renderer.rb +34 -0
  43. data/lib/action_view/renderer/partial_renderer.rb +20 -282
  44. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +25 -26
  45. data/lib/action_view/renderer/renderer.rb +44 -1
  46. data/lib/action_view/renderer/streaming_template_renderer.rb +5 -1
  47. data/lib/action_view/renderer/template_renderer.rb +15 -12
  48. data/lib/action_view/rendering.rb +3 -1
  49. data/lib/action_view/routing_url_for.rb +1 -1
  50. data/lib/action_view/template.rb +9 -49
  51. data/lib/action_view/template/handlers.rb +0 -26
  52. data/lib/action_view/template/handlers/erb.rb +10 -14
  53. data/lib/action_view/template/handlers/erb/erubi.rb +9 -7
  54. data/lib/action_view/template/html.rb +1 -11
  55. data/lib/action_view/template/raw_file.rb +0 -3
  56. data/lib/action_view/template/renderable.rb +24 -0
  57. data/lib/action_view/template/resolver.rb +82 -40
  58. data/lib/action_view/template/text.rb +0 -3
  59. data/lib/action_view/test_case.rb +18 -25
  60. data/lib/action_view/testing/resolvers.rb +10 -31
  61. data/lib/action_view/unbound_template.rb +3 -3
  62. data/lib/action_view/view_paths.rb +34 -36
  63. metadata +18 -15
@@ -253,7 +253,7 @@ module ActionView
253
253
  end
254
254
 
255
255
  # Formats the bytes in +number+ into a more understandable
256
- # representation (e.g., giving it 1500 yields 1.5 KB). This
256
+ # representation (e.g., giving it 1500 yields 1.46 KB). This
257
257
  # method is useful for reporting file sizes to users. You can
258
258
  # customize the format in the +options+ hash.
259
259
  #
@@ -299,7 +299,7 @@ module ActionView
299
299
  end
300
300
 
301
301
  # Pretty prints (formats and approximates) a number in a way it
302
- # is more readable by humans (eg.: 1200000000 becomes "1.2
302
+ # is more readable by humans (e.g.: 1200000000 becomes "1.2
303
303
  # Billion"). This is useful for numbers that can get very large
304
304
  # (and too hard to read).
305
305
  #
@@ -307,7 +307,7 @@ module ActionView
307
307
  # size.
308
308
  #
309
309
  # You can also define your own unit-quantifier names if you want
310
- # to use other decimal units (eg.: 1500 becomes "1.5
310
+ # to use other decimal units (e.g.: 1500 becomes "1.5
311
311
  # kilometers", 0.150 becomes "150 milliliters", etc). You may
312
312
  # define a wide range of unit quantifiers, even fractional ones
313
313
  # (centi, deci, mili, etc).
@@ -425,9 +425,9 @@ module ActionView
425
425
  end
426
426
 
427
427
  def escape_units(units)
428
- Hash[units.map do |k, v|
429
- [k, ERB::Util.html_escape(v)]
430
- end]
428
+ units.transform_values do |v|
429
+ ERB::Util.html_escape(v)
430
+ end
431
431
  end
432
432
 
433
433
  def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
@@ -22,8 +22,12 @@ module ActionView
22
22
  # type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt>
23
23
  # object.
24
24
  #
25
- # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
26
- # as the locals hash.
25
+ # If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
26
+ #
27
+ # If an object responding to `render_in` is passed, `render_in` is called on the object,
28
+ # passing in the current view context.
29
+ #
30
+ # Otherwise, a partial is rendered using the second parameter as the locals hash.
27
31
  def render(options = {}, locals = {}, &block)
28
32
  case options
29
33
  when Hash
@@ -35,7 +39,11 @@ module ActionView
35
39
  end
36
40
  end
37
41
  else
38
- view_renderer.render_partial(self, partial: options, locals: locals, &block)
42
+ if options.respond_to?(:render_in)
43
+ options.render_in(self, &block)
44
+ else
45
+ view_renderer.render_partial(self, partial: options, locals: locals, &block)
46
+ end
39
47
  end
40
48
  end
41
49
 
@@ -129,11 +129,11 @@ module ActionView
129
129
  end
130
130
 
131
131
  def sanitized_allowed_tags
132
- safe_list_sanitizer.allowed_tags
132
+ sanitizer_vendor.safe_list_sanitizer.allowed_tags
133
133
  end
134
134
 
135
135
  def sanitized_allowed_attributes
136
- safe_list_sanitizer.allowed_attributes
136
+ sanitizer_vendor.safe_list_sanitizer.allowed_attributes
137
137
  end
138
138
 
139
139
  # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
@@ -13,19 +13,27 @@ module ActionView
13
13
  include CaptureHelper
14
14
  include OutputSafetyHelper
15
15
 
16
- BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked
17
- compact controls declare default defaultchecked
18
- defaultmuted defaultselected defer disabled
19
- enabled formnovalidate hidden indeterminate inert
20
- ismap itemscope loop multiple muted nohref
21
- noresize noshade novalidate nowrap open
22
- pauseonexit readonly required reversed scoped
23
- seamless selected sortable truespeed typemustmatch
24
- visible).to_set
16
+ BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
17
+ autoplay checked compact controls declare default
18
+ defaultchecked defaultmuted defaultselected defer
19
+ disabled enabled formnovalidate hidden indeterminate
20
+ inert ismap itemscope loop multiple muted nohref
21
+ nomodule noresize noshade novalidate nowrap open
22
+ pauseonexit playsinline readonly required reversed
23
+ scoped seamless selected sortable truespeed
24
+ typemustmatch visible).to_set
25
25
 
26
26
  BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
27
+ BOOLEAN_ATTRIBUTES.freeze
27
28
 
28
- TAG_PREFIXES = ["aria", "data", :aria, :data].to_set
29
+ ARIA_PREFIXES = ["aria", :aria].to_set.freeze
30
+ DATA_PREFIXES = ["data", :data].to_set.freeze
31
+
32
+ TAG_TYPES = {}
33
+ TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
34
+ TAG_TYPES.merge! DATA_PREFIXES.index_with(:data)
35
+ TAG_TYPES.merge! ARIA_PREFIXES.index_with(:aria)
36
+ TAG_TYPES.freeze
29
37
 
30
38
  PRE_CONTENT_STRINGS = Hash.new { "" }
31
39
  PRE_CONTENT_STRINGS[:textarea] = "\n"
@@ -41,6 +49,10 @@ module ActionView
41
49
  @view_context = view_context
42
50
  end
43
51
 
52
+ def p(*arguments, **options, &block)
53
+ tag_string(:p, *arguments, **options, &block)
54
+ end
55
+
44
56
  def tag_string(name, content = nil, escape_attributes: true, **options, &block)
45
57
  content = @view_context.capture(self, &block) if block_given?
46
58
  if VOID_ELEMENTS.include?(name) && content.nil?
@@ -61,13 +73,31 @@ module ActionView
61
73
  output = +""
62
74
  sep = " "
63
75
  options.each_pair do |key, value|
64
- if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
76
+ type = TAG_TYPES[key]
77
+ if type == :data && value.is_a?(Hash)
78
+ value.each_pair do |k, v|
79
+ next if v.nil?
80
+ output << sep
81
+ output << prefix_tag_option(key, k, v, escape)
82
+ end
83
+ elsif type == :aria && value.is_a?(Hash)
65
84
  value.each_pair do |k, v|
66
85
  next if v.nil?
86
+
87
+ case v
88
+ when Array, Hash
89
+ tokens = TagHelper.build_tag_values(v)
90
+ next if tokens.none?
91
+
92
+ v = safe_join(tokens, " ")
93
+ else
94
+ v = v.to_s
95
+ end
96
+
67
97
  output << sep
68
98
  output << prefix_tag_option(key, k, v, escape)
69
99
  end
70
- elsif BOOLEAN_ATTRIBUTES.include?(key)
100
+ elsif type == :boolean
71
101
  if value
72
102
  output << sep
73
103
  output << boolean_tag_option(key)
@@ -85,12 +115,14 @@ module ActionView
85
115
  end
86
116
 
87
117
  def tag_option(key, value, escape)
88
- if value.is_a?(Array)
118
+ case value
119
+ when Array, Hash
120
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
89
121
  value = escape ? safe_join(value, " ") : value.join(" ")
90
122
  else
91
- value = escape ? ERB::Util.unwrapped_html_escape(value).dup : value.to_s.dup
123
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
92
124
  end
93
- value.gsub!('"', "&quot;")
125
+ value = value.gsub('"', "&quot;") if value.include?('"')
94
126
  %(#{key}="#{value}")
95
127
  end
96
128
 
@@ -152,8 +184,8 @@ module ActionView
152
184
  # tag.input type: 'text', disabled: true
153
185
  # # => <input type="text" disabled="disabled">
154
186
  #
155
- # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
156
- # pointing to a hash of sub-attributes.
187
+ # HTML5 <tt>data-*</tt> and <tt>aria-*</tt> attributes can be set with a
188
+ # single +data+ or +aria+ key pointing to a hash of sub-attributes.
157
189
  #
158
190
  # To play nicely with JavaScript conventions, sub-attributes are dasherized.
159
191
  #
@@ -233,6 +265,9 @@ module ActionView
233
265
  #
234
266
  # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
235
267
  # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
268
+ #
269
+ # tag("div", class: { highlight: current_user.admin? })
270
+ # # => <div class="highlight" />
236
271
  def tag(name = nil, options = nil, open = false, escape = true)
237
272
  if name.nil?
238
273
  tag_builder
@@ -260,6 +295,8 @@ module ActionView
260
295
  # # => <div class="strong"><p>Hello world!</p></div>
261
296
  # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
262
297
  # # => <div class="strong highlight">Hello world!</div>
298
+ # content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
299
+ # # => <div class="strong highlight">Hello world!</div>
263
300
  # content_tag("select", options, multiple: true)
264
301
  # # => <select multiple="multiple">...options...</select>
265
302
  #
@@ -276,6 +313,24 @@ module ActionView
276
313
  end
277
314
  end
278
315
 
316
+ # Returns a string of tokens built from +args+.
317
+ #
318
+ # ==== Examples
319
+ # token_list("foo", "bar")
320
+ # # => "foo bar"
321
+ # token_list("foo", "foo bar")
322
+ # # => "foo bar"
323
+ # token_list({ foo: true, bar: false })
324
+ # # => "foo"
325
+ # token_list(nil, false, 123, "", "foo", { bar: true })
326
+ # # => "123 foo bar"
327
+ def token_list(*args)
328
+ tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
329
+
330
+ safe_join(tokens, " ")
331
+ end
332
+ alias_method :class_names, :token_list
333
+
279
334
  # Returns a CDATA section with the given +content+. CDATA sections
280
335
  # are used to escape blocks of text containing characters which would
281
336
  # otherwise be recognized as markup. CDATA sections begin with the string
@@ -306,6 +361,26 @@ module ActionView
306
361
  end
307
362
 
308
363
  private
364
+ def build_tag_values(*args)
365
+ tag_values = []
366
+
367
+ args.each do |tag_value|
368
+ case tag_value
369
+ when Hash
370
+ tag_value.each do |key, val|
371
+ tag_values << key.to_s if val && key.present?
372
+ end
373
+ when Array
374
+ tag_values.concat build_tag_values(*tag_value)
375
+ else
376
+ tag_values << tag_value.to_s if tag_value.present?
377
+ end
378
+ end
379
+
380
+ tag_values
381
+ end
382
+ module_function :build_tag_values
383
+
309
384
  def tag_builder
310
385
  @tag_builder ||= TagBuilder.new(self)
311
386
  end
@@ -105,7 +105,7 @@ module ActionView
105
105
  end
106
106
 
107
107
  def tag_name(multiple = false, index = nil)
108
- # a little duplication to construct less strings
108
+ # a little duplication to construct fewer strings
109
109
  case
110
110
  when @object_name.empty?
111
111
  "#{sanitized_method_name}#{multiple ? "[]" : ""}"
@@ -117,7 +117,7 @@ module ActionView
117
117
  end
118
118
 
119
119
  def tag_id(index = nil)
120
- # a little duplication to construct less strings
120
+ # a little duplication to construct fewer strings
121
121
  case
122
122
  when @object_name.empty?
123
123
  sanitized_method_name.dup
@@ -129,11 +129,11 @@ module ActionView
129
129
  end
130
130
 
131
131
  def sanitized_object_name
132
- @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
132
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
133
133
  end
134
134
 
135
135
  def sanitized_method_name
136
- @sanitized_method_name ||= @method_name.sub(/\?$/, "")
136
+ @sanitized_method_name ||= @method_name.delete_suffix("?")
137
137
  end
138
138
 
139
139
  def sanitized_value(value)
@@ -166,8 +166,11 @@ module ActionView
166
166
 
167
167
  def add_options(option_tags, options, value = nil)
168
168
  if options[:include_blank]
169
- option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags
169
+ content = (options[:include_blank] if options[:include_blank].is_a?(String))
170
+ label = (" " unless content)
171
+ option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
170
172
  end
173
+
171
174
  if value.blank? && options[:prompt]
172
175
  tag_options = { value: "" }.tap do |prompt_opts|
173
176
  prompt_opts[:disabled] = true if options[:disabled] == ""
@@ -175,6 +178,7 @@ module ActionView
175
178
  end
176
179
  option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
177
180
  end
181
+
178
182
  option_tags
179
183
  end
180
184
 
@@ -6,7 +6,7 @@ module ActionView
6
6
  class DateField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%Y-%m-%d")
9
+ value&.strftime("%Y-%m-%d")
10
10
  end
11
11
  end
12
12
  end
@@ -13,7 +13,7 @@ module ActionView
13
13
  end
14
14
 
15
15
  def render
16
- error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
16
+ error_wrapping(datetime_selector(@options, @html_options).public_send("select_#{select_type}").html_safe)
17
17
  end
18
18
 
19
19
  class << self
@@ -58,7 +58,7 @@ module ActionView
58
58
  time = Time.current
59
59
 
60
60
  [:year, :month, :day, :hour, :min, :sec].each do |key|
61
- default[key] ||= time.send(key)
61
+ default[key] ||= time.public_send(key)
62
62
  end
63
63
 
64
64
  Time.utc(
@@ -12,7 +12,7 @@ module ActionView
12
12
 
13
13
  private
14
14
  def format_date(value)
15
- value.try(:strftime, "%Y-%m-%dT%T")
15
+ value&.strftime("%Y-%m-%dT%T")
16
16
  end
17
17
  end
18
18
  end
@@ -25,6 +25,10 @@ module ActionView
25
25
 
26
26
  content
27
27
  end
28
+
29
+ def to_s
30
+ translation
31
+ end
28
32
  end
29
33
 
30
34
  def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
@@ -6,7 +6,7 @@ module ActionView
6
6
  class MonthField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%Y-%m")
9
+ value&.strftime("%Y-%m")
10
10
  end
11
11
  end
12
12
  end
@@ -15,7 +15,7 @@ module ActionView
15
15
 
16
16
  def render
17
17
  option_tags_options = {
18
- selected: @options.fetch(:selected) { value },
18
+ selected: @options.fetch(:selected) { value.nil? ? "" : value },
19
19
  disabled: @options[:disabled]
20
20
  }
21
21
 
@@ -6,7 +6,7 @@ module ActionView
6
6
  class TimeField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%T.%L")
9
+ value&.strftime("%T.%L")
10
10
  end
11
11
  end
12
12
  end
@@ -6,7 +6,7 @@ module ActionView
6
6
  class WeekField < DatetimeField # :nodoc:
7
7
  private
8
8
  def format_date(value)
9
- value.try(:strftime, "%Y-W%V")
9
+ value&.strftime("%Y-W%V")
10
10
  end
11
11
  end
12
12
  end
@@ -228,7 +228,7 @@ module ActionView
228
228
  # pluralize(2, 'Person', locale: :de)
229
229
  # # => 2 Personen
230
230
  def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
231
- word = if count == 1 || count.to_s =~ /^1(\.0+)?$/
231
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
232
232
  singular
233
233
  else
234
234
  plural || singular.pluralize(locale)
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "action_view/helpers/tag_helper"
4
- require "active_support/core_ext/string/access"
5
- require "i18n/exceptions"
4
+ require "active_support/core_ext/symbol/starts_ends_with"
6
5
 
7
6
  module ActionView
8
7
  # = Action View Translation Helpers
@@ -57,64 +56,56 @@ module ActionView
57
56
  # that include HTML tags so that you know what kind of output to expect
58
57
  # when you call translate in a template and translators know which keys
59
58
  # they can provide HTML values for.
59
+ #
60
+ # To access the translated text along with the fully resolved
61
+ # translation key, <tt>translate</tt> accepts a block:
62
+ #
63
+ # <%= translate(".relative_key") do |translation, resolved_key| %>
64
+ # <span title="<%= resolved_key %>"><%= translation %></span>
65
+ # <% end %>
66
+ #
67
+ # This enables annotate translated text to be aware of the scope it was
68
+ # resolved against.
69
+ #
60
70
  def translate(key, **options)
61
- if options.has_key?(:default)
62
- remaining_defaults = Array.wrap(options.delete(:default)).compact
63
- options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
64
- end
71
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
72
+ key = key&.to_s unless key.is_a?(Symbol)
65
73
 
66
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
67
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
68
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
69
- if options[:raise] == false
70
- raise_error = false
71
- i18n_raise = false
72
- else
73
- raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
74
- i18n_raise = true
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
75
76
  end
76
77
 
77
- if html_safe_translation_key?(key)
78
- html_safe_options = options.dup
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
79
80
 
80
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
81
- unless name == :count && value.is_a?(Numeric)
82
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
83
- end
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
84
84
  end
85
85
 
86
- html_safe_options[:default] = MISSING_TRANSLATION unless html_safe_options[:default].blank?
87
-
88
- translation = I18n.translate(scope_key_by_partial(key), **html_safe_options.merge(raise: i18n_raise))
86
+ key = scope_key_by_partial(key)
89
87
 
90
- if translation.equal?(MISSING_TRANSLATION)
91
- options[:default].first
92
- elsif translation.respond_to?(:map)
93
- translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
88
+ if html_safe_translation_key?(key)
89
+ html_safe_options ||= html_escape_translation_options(options)
90
+ translated = I18n.translate(key, **html_safe_options, default: default)
91
+ break html_safe_translation(translated) unless translated.equal?(MISSING_TRANSLATION)
94
92
  else
95
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
96
- end
97
- else
98
- I18n.translate(scope_key_by_partial(key), **options.merge(raise: i18n_raise))
99
- end
100
- rescue I18n::MissingTranslationData => e
101
- if remaining_defaults.present?
102
- translate remaining_defaults.shift, **options.merge(default: remaining_defaults)
103
- else
104
- raise e if raise_error
105
-
106
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
107
- title = +"translation missing: #{keys.join('.')}"
108
-
109
- interpolations = options.except(:default, :scope)
110
- if interpolations.any?
111
- title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
93
+ translated = I18n.translate(key, **options, default: default)
94
+ break translated unless translated.equal?(MISSING_TRANSLATION)
112
95
  end
113
96
 
114
- return title unless ActionView::Base.debug_missing_translation
97
+ break alternatives.first if alternatives.present? && !alternatives.first.is_a?(Symbol)
115
98
 
116
- content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
99
+ first_key ||= key
100
+ key = alternatives&.shift
117
101
  end
102
+
103
+ if key.nil? && !first_key.nil?
104
+ translation = missing_translation(first_key, options)
105
+ key = first_key
106
+ end
107
+
108
+ block_given? ? yield(translation, key) : translation
118
109
  end
119
110
  alias :t :translate
120
111
 
@@ -131,13 +122,19 @@ module ActionView
131
122
  MISSING_TRANSLATION = Object.new
132
123
  private_constant :MISSING_TRANSLATION
133
124
 
125
+ NO_DEFAULT = [].freeze
126
+ private_constant :NO_DEFAULT
127
+
128
+ def self.i18n_option?(name)
129
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
130
+ end
131
+
134
132
  def scope_key_by_partial(key)
135
- stringified_key = key.to_s
136
- if stringified_key.first == "."
133
+ if key&.start_with?(".")
137
134
  if @virtual_path
138
135
  @_scope_key_by_partial_cache ||= {}
139
136
  @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
140
- "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}"
137
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
141
138
  else
142
139
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
143
140
  end
@@ -146,8 +143,47 @@ module ActionView
146
143
  end
147
144
  end
148
145
 
146
+ def html_escape_translation_options(options)
147
+ return options if options.empty?
148
+ html_safe_options = options.dup
149
+
150
+ options.each do |name, value|
151
+ unless TranslationHelper.i18n_option?(name) || (name == :count && value.is_a?(Numeric))
152
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
153
+ end
154
+ end
155
+
156
+ html_safe_options
157
+ end
158
+
149
159
  def html_safe_translation_key?(key)
150
- /(?:_|\b)html\z/.match?(key.to_s)
160
+ /(?:_|\b)html\z/.match?(key)
161
+ end
162
+
163
+ def html_safe_translation(translation)
164
+ if translation.respond_to?(:map)
165
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
166
+ else
167
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
168
+ end
169
+ end
170
+
171
+ def missing_translation(key, options)
172
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
173
+
174
+ title = +"translation missing: #{keys.join(".")}"
175
+
176
+ options.each do |name, value|
177
+ unless name == :scope
178
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
179
+ end
180
+ end
181
+
182
+ if ActionView::Base.debug_missing_translation
183
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
184
+ else
185
+ title
186
+ end
151
187
  end
152
188
  end
153
189
  end