actionview 5.2.7.1 → 6.1.4.6

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +250 -112
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -3
  5. data/lib/action_view/base.rb +81 -15
  6. data/lib/action_view/buffers.rb +15 -0
  7. data/lib/action_view/cache_expiry.rb +52 -0
  8. data/lib/action_view/context.rb +5 -9
  9. data/lib/action_view/dependency_tracker.rb +10 -4
  10. data/lib/action_view/digestor.rb +15 -22
  11. data/lib/action_view/flows.rb +0 -1
  12. data/lib/action_view/gem_version.rb +4 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +0 -1
  14. data/lib/action_view/helpers/asset_tag_helper.rb +64 -47
  15. data/lib/action_view/helpers/asset_url_helper.rb +9 -6
  16. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  17. data/lib/action_view/helpers/cache_helper.rb +23 -22
  18. data/lib/action_view/helpers/capture_helper.rb +4 -0
  19. data/lib/action_view/helpers/csp_helper.rb +4 -2
  20. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  21. data/lib/action_view/helpers/date_helper.rb +73 -30
  22. data/lib/action_view/helpers/form_helper.rb +305 -37
  23. data/lib/action_view/helpers/form_options_helper.rb +23 -23
  24. data/lib/action_view/helpers/form_tag_helper.rb +19 -16
  25. data/lib/action_view/helpers/javascript_helper.rb +12 -11
  26. data/lib/action_view/helpers/number_helper.rb +14 -8
  27. data/lib/action_view/helpers/output_safety_helper.rb +1 -1
  28. data/lib/action_view/helpers/rendering_helper.rb +17 -7
  29. data/lib/action_view/helpers/sanitize_helper.rb +12 -18
  30. data/lib/action_view/helpers/tag_helper.rb +100 -55
  31. data/lib/action_view/helpers/tags/base.rb +18 -11
  32. data/lib/action_view/helpers/tags/check_box.rb +0 -1
  33. data/lib/action_view/helpers/tags/collection_check_boxes.rb +0 -1
  34. data/lib/action_view/helpers/tags/collection_helpers.rb +0 -1
  35. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +0 -1
  36. data/lib/action_view/helpers/tags/color_field.rb +1 -2
  37. data/lib/action_view/helpers/tags/date_field.rb +1 -2
  38. data/lib/action_view/helpers/tags/date_select.rb +2 -3
  39. data/lib/action_view/helpers/tags/datetime_field.rb +0 -1
  40. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -2
  41. data/lib/action_view/helpers/tags/label.rb +4 -1
  42. data/lib/action_view/helpers/tags/month_field.rb +1 -2
  43. data/lib/action_view/helpers/tags/radio_button.rb +0 -1
  44. data/lib/action_view/helpers/tags/select.rb +1 -2
  45. data/lib/action_view/helpers/tags/text_field.rb +0 -1
  46. data/lib/action_view/helpers/tags/time_field.rb +1 -2
  47. data/lib/action_view/helpers/tags/translator.rb +1 -6
  48. data/lib/action_view/helpers/tags/week_field.rb +1 -2
  49. data/lib/action_view/helpers/text_helper.rb +4 -5
  50. data/lib/action_view/helpers/translation_helper.rb +94 -54
  51. data/lib/action_view/helpers/url_helper.rb +136 -28
  52. data/lib/action_view/helpers.rb +0 -2
  53. data/lib/action_view/layouts.rb +8 -10
  54. data/lib/action_view/log_subscriber.rb +30 -15
  55. data/lib/action_view/lookup_context.rb +63 -35
  56. data/lib/action_view/path_set.rb +3 -12
  57. data/lib/action_view/railtie.rb +42 -26
  58. data/lib/action_view/record_identifier.rb +2 -3
  59. data/lib/action_view/renderer/abstract_renderer.rb +142 -11
  60. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  61. data/lib/action_view/renderer/object_renderer.rb +34 -0
  62. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +61 -16
  63. data/lib/action_view/renderer/partial_renderer.rb +21 -273
  64. data/lib/action_view/renderer/renderer.rb +59 -4
  65. data/lib/action_view/renderer/streaming_template_renderer.rb +10 -8
  66. data/lib/action_view/renderer/template_renderer.rb +35 -27
  67. data/lib/action_view/rendering.rb +54 -33
  68. data/lib/action_view/routing_url_for.rb +13 -12
  69. data/lib/action_view/template/error.rb +30 -15
  70. data/lib/action_view/template/handlers/builder.rb +2 -2
  71. data/lib/action_view/template/handlers/erb/erubi.rb +15 -9
  72. data/lib/action_view/template/handlers/erb.rb +16 -11
  73. data/lib/action_view/template/handlers/html.rb +1 -1
  74. data/lib/action_view/template/handlers/raw.rb +2 -2
  75. data/lib/action_view/template/handlers.rb +1 -1
  76. data/lib/action_view/template/html.rb +5 -6
  77. data/lib/action_view/template/inline.rb +22 -0
  78. data/lib/action_view/template/raw_file.rb +25 -0
  79. data/lib/action_view/template/renderable.rb +24 -0
  80. data/lib/action_view/template/resolver.rb +191 -150
  81. data/lib/action_view/template/sources/file.rb +17 -0
  82. data/lib/action_view/template/sources.rb +13 -0
  83. data/lib/action_view/template/text.rb +2 -3
  84. data/lib/action_view/template.rb +66 -75
  85. data/lib/action_view/test_case.rb +21 -29
  86. data/lib/action_view/testing/resolvers.rb +18 -27
  87. data/lib/action_view/unbound_template.rb +31 -0
  88. data/lib/action_view/view_paths.rb +59 -38
  89. data/lib/action_view.rb +7 -2
  90. data/lib/assets/compiled/rails-ujs.js +32 -6
  91. metadata +29 -18
  92. data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -34,7 +34,6 @@ module ActionView
34
34
  end
35
35
 
36
36
  private
37
-
38
37
  def value
39
38
  if @allow_method_names_outside_object
40
39
  object.public_send @method_name if object && object.respond_to?(@method_name)
@@ -106,19 +105,19 @@ module ActionView
106
105
  end
107
106
 
108
107
  def tag_name(multiple = false, index = nil)
109
- # a little duplication to construct less strings
108
+ # a little duplication to construct fewer strings
110
109
  case
111
110
  when @object_name.empty?
112
- "#{sanitized_method_name}#{"[]" if multiple}"
111
+ "#{sanitized_method_name}#{multiple ? "[]" : ""}"
113
112
  when index
114
- "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
113
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
115
114
  else
116
- "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
115
+ "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
117
116
  end
118
117
  end
119
118
 
120
119
  def tag_id(index = nil)
121
- # a little duplication to construct less strings
120
+ # a little duplication to construct fewer strings
122
121
  case
123
122
  when @object_name.empty?
124
123
  sanitized_method_name.dup
@@ -130,15 +129,15 @@ module ActionView
130
129
  end
131
130
 
132
131
  def sanitized_object_name
133
- @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
132
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
134
133
  end
135
134
 
136
135
  def sanitized_method_name
137
- @sanitized_method_name ||= @method_name.sub(/\?$/, "")
136
+ @sanitized_method_name ||= @method_name.delete_suffix("?")
138
137
  end
139
138
 
140
139
  def sanitized_value(value)
141
- value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s
140
+ value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
142
141
  end
143
142
 
144
143
  def select_content_tag(option_tags, options, html_options)
@@ -167,11 +166,19 @@ module ActionView
167
166
 
168
167
  def add_options(option_tags, options, value = nil)
169
168
  if options[:include_blank]
170
- 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
171
172
  end
173
+
172
174
  if value.blank? && options[:prompt]
173
- option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), value: "") + "\n" + option_tags
175
+ tag_options = { value: "" }.tap do |prompt_opts|
176
+ prompt_opts[:disabled] = true if options[:disabled] == ""
177
+ prompt_opts[:selected] = true if options[:selected] == ""
178
+ end
179
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
174
180
  end
181
+
175
182
  option_tags
176
183
  end
177
184
 
@@ -39,7 +39,6 @@ module ActionView
39
39
  end
40
40
 
41
41
  private
42
-
43
42
  def checked?(value)
44
43
  case value
45
44
  when TrueClass, FalseClass
@@ -22,7 +22,6 @@ module ActionView
22
22
  end
23
23
 
24
24
  private
25
-
26
25
  def render_component(builder)
27
26
  builder.check_box + builder.label
28
27
  end
@@ -37,7 +37,6 @@ module ActionView
37
37
  end
38
38
 
39
39
  private
40
-
41
40
  def instantiate_builder(builder_class, item, value, text, html_options)
42
41
  builder_class.new(@template_object, @object_name, @method_name, item,
43
42
  sanitize_attribute_name(value), text, value, html_options)
@@ -21,7 +21,6 @@ module ActionView
21
21
  end
22
22
 
23
23
  private
24
-
25
24
  def render_component(builder)
26
25
  builder.radio_button + builder.label
27
26
  end
@@ -12,10 +12,9 @@ module ActionView
12
12
  end
13
13
 
14
14
  private
15
-
16
15
  def validate_color_string(string)
17
16
  regex = /#[0-9a-fA-F]{6}/
18
- if regex.match(string)
17
+ if regex.match?(string)
19
18
  string.downcase
20
19
  else
21
20
  "#000000"
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class DateField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%Y-%m-%d")
9
+ value&.strftime("%Y-%m-%d")
11
10
  end
12
11
  end
13
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
@@ -23,7 +23,6 @@ module ActionView
23
23
  end
24
24
 
25
25
  private
26
-
27
26
  def select_type
28
27
  self.class.select_type
29
28
  end
@@ -59,7 +58,7 @@ module ActionView
59
58
  time = Time.current
60
59
 
61
60
  [:year, :month, :day, :hour, :min, :sec].each do |key|
62
- default[key] ||= time.send(key)
61
+ default[key] ||= time.public_send(key)
63
62
  end
64
63
 
65
64
  Time.utc(
@@ -14,7 +14,6 @@ module ActionView
14
14
  end
15
15
 
16
16
  private
17
-
18
17
  def format_date(value)
19
18
  raise NotImplementedError
20
19
  end
@@ -11,9 +11,8 @@ module ActionView
11
11
  end
12
12
 
13
13
  private
14
-
15
14
  def format_date(value)
16
- value.try(:strftime, "%Y-%m-%dT%T")
15
+ value&.strftime("%Y-%m-%dT%T")
17
16
  end
18
17
  end
19
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)
@@ -71,7 +75,6 @@ module ActionView
71
75
  end
72
76
 
73
77
  private
74
-
75
78
  def render_component(builder)
76
79
  builder.translation
77
80
  end
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class MonthField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%Y-%m")
9
+ value&.strftime("%Y-%m")
11
10
  end
12
11
  end
13
12
  end
@@ -23,7 +23,6 @@ module ActionView
23
23
  end
24
24
 
25
25
  private
26
-
27
26
  def checked?(value)
28
27
  value.to_s == @tag_value.to_s
29
28
  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
 
@@ -29,7 +29,6 @@ module ActionView
29
29
  end
30
30
 
31
31
  private
32
-
33
32
  # Grouped choices look like this:
34
33
  #
35
34
  # [nil, []]
@@ -24,7 +24,6 @@ module ActionView
24
24
  end
25
25
 
26
26
  private
27
-
28
27
  def field_type
29
28
  self.class.field_type
30
29
  end
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class TimeField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%T.%L")
9
+ value&.strftime("%T.%L")
11
10
  end
12
11
  end
13
12
  end
@@ -16,13 +16,8 @@ module ActionView
16
16
  translated_attribute || human_attribute_name
17
17
  end
18
18
 
19
- # TODO Change this to private once we've dropped Ruby 2.2 support.
20
- # Workaround for Ruby 2.2 "private attribute?" warning.
21
- protected
22
-
23
- attr_reader :object_name, :method_and_value, :scope, :model
24
-
25
19
  private
20
+ attr_reader :object_name, :method_and_value, :scope, :model
26
21
 
27
22
  def i18n_default
28
23
  if model
@@ -5,9 +5,8 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class WeekField < DatetimeField # :nodoc:
7
7
  private
8
-
9
8
  def format_date(value)
10
- value.try(:strftime, "%Y-W%V")
9
+ value&.strftime("%Y-W%V")
11
10
  end
12
11
  end
13
12
  end
@@ -105,7 +105,7 @@ module ActionView
105
105
  # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
106
106
  # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
107
107
  # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
108
- # '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+
108
+ # <tt><mark>\1</mark></tt>) or passing a block that receives each matched term. By default +text+
109
109
  # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
110
110
  # for <tt>:sanitize</tt> will turn sanitizing off.
111
111
  #
@@ -188,7 +188,7 @@ module ActionView
188
188
 
189
189
  unless separator.empty?
190
190
  text.split(separator).each do |value|
191
- if value.match(regex)
191
+ if value.match?(regex)
192
192
  phrase = value
193
193
  break
194
194
  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)
@@ -259,7 +259,7 @@ module ActionView
259
259
  # # => Once\r\nupon\r\na\r\ntime
260
260
  def word_wrap(text, line_width: 80, break_sequence: "\n")
261
261
  text.split("\n").collect! do |line|
262
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line
262
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
263
263
  end * break_sequence
264
264
  end
265
265
 
@@ -426,7 +426,6 @@ module ActionView
426
426
  end
427
427
 
428
428
  private
429
-
430
429
  def next_index
431
430
  step_index(1)
432
431
  end
@@ -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,74 +56,67 @@ 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.
60
- def translate(key, options = {})
61
- options = options.dup
62
- has_default = options.has_key?(:default)
63
- remaining_defaults = Array(options.delete(:default)).compact
64
-
65
- if has_default && !remaining_defaults.first.kind_of?(Symbol)
66
- options[:default] = remaining_defaults
67
- end
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
+ #
70
+ def translate(key, **options)
71
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
72
+ key = key&.to_s unless key.is_a?(Symbol)
68
73
 
69
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
70
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
71
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
72
- if options[:raise] == false
73
- raise_error = false
74
- i18n_raise = false
75
- else
76
- raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
77
- i18n_raise = true
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
78
76
  end
79
77
 
80
- if html_safe_translation_key?(key)
81
- html_safe_options = options.dup
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
82
80
 
83
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
84
- unless name == :count && value.is_a?(Numeric)
85
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
86
- end
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
87
84
  end
88
85
 
89
- html_safe_options[:default] = MISSING_TRANSLATION unless html_safe_options[:default].blank?
90
-
91
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
86
+ key = scope_key_by_partial(key)
92
87
 
93
- if translation.equal?(MISSING_TRANSLATION)
94
- options[:default].first
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)
95
92
  else
96
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
93
+ translated = I18n.translate(key, **options, default: default)
94
+ break translated unless translated.equal?(MISSING_TRANSLATION)
97
95
  end
98
- else
99
- I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
100
- end
101
- rescue I18n::MissingTranslationData => e
102
- if remaining_defaults.present?
103
- translate remaining_defaults.shift, options.merge(default: remaining_defaults)
104
- else
105
- raise e if raise_error
106
-
107
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
108
- title = "translation missing: #{keys.join('.')}".dup
109
-
110
- interpolations = options.except(:default, :scope)
111
- if interpolations.any?
112
- title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
96
+
97
+ if alternatives.present? && !alternatives.first.is_a?(Symbol)
98
+ break alternatives.first && I18n.translate(**options, default: alternatives)
113
99
  end
114
100
 
115
- return title unless ActionView::Base.debug_missing_translation
101
+ first_key ||= key
102
+ key = alternatives&.shift
103
+ end
116
104
 
117
- content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
105
+ if key.nil? && !first_key.nil?
106
+ translation = missing_translation(first_key, options)
107
+ key = first_key
118
108
  end
109
+
110
+ block_given? ? yield(translation, key) : translation
119
111
  end
120
112
  alias :t :translate
121
113
 
122
114
  # Delegates to <tt>I18n.localize</tt> with no additional functionality.
123
115
  #
124
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
116
+ # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
125
117
  # for more information.
126
- def localize(*args)
127
- I18n.localize(*args)
118
+ def localize(object, **options)
119
+ I18n.localize(object, **options)
128
120
  end
129
121
  alias :l :localize
130
122
 
@@ -132,10 +124,19 @@ module ActionView
132
124
  MISSING_TRANSLATION = Object.new
133
125
  private_constant :MISSING_TRANSLATION
134
126
 
127
+ NO_DEFAULT = [].freeze
128
+ private_constant :NO_DEFAULT
129
+
130
+ def self.i18n_option?(name)
131
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
132
+ end
133
+
135
134
  def scope_key_by_partial(key)
136
- if key.to_s.first == "."
135
+ if key&.start_with?(".")
137
136
  if @virtual_path
138
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
137
+ @_scope_key_by_partial_cache ||= {}
138
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
139
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
139
140
  else
140
141
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
141
142
  end
@@ -144,8 +145,47 @@ module ActionView
144
145
  end
145
146
  end
146
147
 
148
+ def html_escape_translation_options(options)
149
+ return options if options.empty?
150
+ html_safe_options = options.dup
151
+
152
+ options.each do |name, value|
153
+ unless TranslationHelper.i18n_option?(name) || (name == :count && value.is_a?(Numeric))
154
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
155
+ end
156
+ end
157
+
158
+ html_safe_options
159
+ end
160
+
147
161
  def html_safe_translation_key?(key)
148
- /(\b|_|\.)html$/.match?(key.to_s)
162
+ /(?:_|\b)html\z/.match?(key)
163
+ end
164
+
165
+ def html_safe_translation(translation)
166
+ if translation.respond_to?(:map)
167
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
168
+ else
169
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
170
+ end
171
+ end
172
+
173
+ def missing_translation(key, options)
174
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
175
+
176
+ title = +"translation missing: #{keys.join(".")}"
177
+
178
+ options.each do |name, value|
179
+ unless name == :scope
180
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
181
+ end
182
+ end
183
+
184
+ if ActionView::Base.debug_missing_translation
185
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
186
+ else
187
+ title
188
+ end
149
189
  end
150
190
  end
151
191
  end