actionview 5.1.4 → 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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +199 -168
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -5
  5. data/lib/action_view.rb +10 -4
  6. data/lib/action_view/base.rb +87 -23
  7. data/lib/action_view/buffers.rb +17 -0
  8. data/lib/action_view/cache_expiry.rb +52 -0
  9. data/lib/action_view/context.rb +7 -11
  10. data/lib/action_view/dependency_tracker.rb +12 -4
  11. data/lib/action_view/digestor.rb +24 -23
  12. data/lib/action_view/flows.rb +2 -1
  13. data/lib/action_view/gem_version.rb +4 -2
  14. data/lib/action_view/helpers.rb +4 -2
  15. data/lib/action_view/helpers/active_model_helper.rb +9 -4
  16. data/lib/action_view/helpers/asset_tag_helper.rb +220 -57
  17. data/lib/action_view/helpers/asset_url_helper.rb +28 -23
  18. data/lib/action_view/helpers/atom_feed_helper.rb +5 -2
  19. data/lib/action_view/helpers/cache_helper.rb +39 -28
  20. data/lib/action_view/helpers/capture_helper.rb +13 -7
  21. data/lib/action_view/helpers/controller_helper.rb +3 -1
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +5 -3
  24. data/lib/action_view/helpers/date_helper.rb +78 -33
  25. data/lib/action_view/helpers/debug_helper.rb +4 -2
  26. data/lib/action_view/helpers/form_helper.rb +357 -106
  27. data/lib/action_view/helpers/form_options_helper.rb +45 -39
  28. data/lib/action_view/helpers/form_tag_helper.rb +42 -27
  29. data/lib/action_view/helpers/javascript_helper.rb +28 -12
  30. data/lib/action_view/helpers/number_helper.rb +16 -8
  31. data/lib/action_view/helpers/output_safety_helper.rb +3 -1
  32. data/lib/action_view/helpers/rendering_helper.rb +20 -9
  33. data/lib/action_view/helpers/sanitize_helper.rb +15 -19
  34. data/lib/action_view/helpers/tag_helper.rb +100 -24
  35. data/lib/action_view/helpers/tags.rb +3 -1
  36. data/lib/action_view/helpers/tags/base.rb +30 -21
  37. data/lib/action_view/helpers/tags/check_box.rb +3 -2
  38. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +2 -1
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +2 -1
  42. data/lib/action_view/helpers/tags/collection_select.rb +3 -1
  43. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/date_select.rb +5 -4
  46. data/lib/action_view/helpers/tags/datetime_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  48. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -1
  52. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/label.rb +6 -5
  54. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  55. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +2 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +3 -2
  59. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +2 -0
  61. data/lib/action_view/helpers/tags/select.rb +4 -3
  62. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +3 -1
  64. data/lib/action_view/helpers/tags/text_field.rb +3 -2
  65. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  66. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  68. data/lib/action_view/helpers/tags/translator.rb +3 -6
  69. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  71. data/lib/action_view/helpers/text_helper.rb +11 -10
  72. data/lib/action_view/helpers/translation_helper.rb +102 -52
  73. data/lib/action_view/helpers/url_helper.rb +150 -32
  74. data/lib/action_view/layouts.rb +15 -15
  75. data/lib/action_view/log_subscriber.rb +32 -15
  76. data/lib/action_view/lookup_context.rb +67 -39
  77. data/lib/action_view/model_naming.rb +2 -0
  78. data/lib/action_view/path_set.rb +5 -12
  79. data/lib/action_view/railtie.rb +46 -21
  80. data/lib/action_view/record_identifier.rb +4 -3
  81. data/lib/action_view/renderer/abstract_renderer.rb +144 -11
  82. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  83. data/lib/action_view/renderer/object_renderer.rb +34 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +33 -283
  85. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +64 -17
  86. data/lib/action_view/renderer/renderer.rb +61 -4
  87. data/lib/action_view/renderer/streaming_template_renderer.rb +14 -8
  88. data/lib/action_view/renderer/template_renderer.rb +36 -26
  89. data/lib/action_view/rendering.rb +57 -38
  90. data/lib/action_view/routing_url_for.rb +15 -12
  91. data/lib/action_view/tasks/cache_digests.rake +2 -0
  92. data/lib/action_view/template.rb +69 -76
  93. data/lib/action_view/template/error.rb +32 -18
  94. data/lib/action_view/template/handlers.rb +4 -2
  95. data/lib/action_view/template/handlers/builder.rb +5 -6
  96. data/lib/action_view/template/handlers/erb.rb +20 -19
  97. data/lib/action_view/template/handlers/erb/erubi.rb +17 -9
  98. data/lib/action_view/template/handlers/html.rb +3 -1
  99. data/lib/action_view/template/handlers/raw.rb +4 -2
  100. data/lib/action_view/template/html.rb +8 -7
  101. data/lib/action_view/template/inline.rb +22 -0
  102. data/lib/action_view/template/raw_file.rb +25 -0
  103. data/lib/action_view/template/renderable.rb +24 -0
  104. data/lib/action_view/template/resolver.rb +194 -152
  105. data/lib/action_view/template/sources.rb +13 -0
  106. data/lib/action_view/template/sources/file.rb +17 -0
  107. data/lib/action_view/template/text.rb +5 -4
  108. data/lib/action_view/template/types.rb +3 -1
  109. data/lib/action_view/test_case.rb +38 -30
  110. data/lib/action_view/testing/resolvers.rb +20 -27
  111. data/lib/action_view/unbound_template.rb +31 -0
  112. data/lib/action_view/version.rb +2 -0
  113. data/lib/action_view/view_paths.rb +61 -40
  114. data/lib/assets/compiled/rails-ujs.js +84 -23
  115. metadata +34 -23
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -21
  117. data/lib/action_view/template/handlers/erb/deprecated_erubis.rb +0 -9
  118. data/lib/action_view/template/handlers/erb/erubis.rb +0 -81
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view/helpers/tags/checkable"
2
4
 
3
5
  module ActionView
@@ -15,13 +17,12 @@ module ActionView
15
17
  options = @options.stringify_keys
16
18
  options["type"] = "radio"
17
19
  options["value"] = @tag_value
18
- options["checked"] = "checked" if input_checked?(object, options)
20
+ options["checked"] = "checked" if input_checked?(options)
19
21
  add_default_name_and_id_for_value(@tag_value, options)
20
22
  tag("input", options)
21
23
  end
22
24
 
23
25
  private
24
-
25
26
  def checked?(value)
26
27
  value.to_s == @tag_value.to_s
27
28
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -13,7 +15,7 @@ module ActionView
13
15
 
14
16
  def render
15
17
  option_tags_options = {
16
- selected: @options.fetch(:selected) { value(@object) },
18
+ selected: @options.fetch(:selected) { value.nil? ? "" : value },
17
19
  disabled: @options[:disabled]
18
20
  }
19
21
 
@@ -27,13 +29,12 @@ module ActionView
27
29
  end
28
30
 
29
31
  private
30
-
31
32
  # Grouped choices look like this:
32
33
  #
33
34
  # [nil, []]
34
35
  # { nil => [] }
35
36
  def grouped_choices?
36
- !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last
37
+ !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
37
38
  end
38
39
  end
39
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view/helpers/tags/placeholderable"
2
4
 
3
5
  module ActionView
@@ -14,7 +16,7 @@ module ActionView
14
16
  options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
15
17
  end
16
18
 
17
- content_tag("textarea", options.delete("value") { value_before_type_cast(object) }, options)
19
+ content_tag("textarea", options.delete("value") { value_before_type_cast }, options)
18
20
  end
19
21
  end
20
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view/helpers/tags/placeholderable"
2
4
 
3
5
  module ActionView
@@ -10,7 +12,7 @@ module ActionView
10
12
  options = @options.stringify_keys
11
13
  options["size"] = options["maxlength"] unless options.key?("size")
12
14
  options["type"] ||= field_type
13
- options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
15
+ options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
14
16
  add_default_name_and_id(options)
15
17
  tag("input", options)
16
18
  end
@@ -22,7 +24,6 @@ module ActionView
22
24
  end
23
25
 
24
26
  private
25
-
26
27
  def field_type
27
28
  self.class.field_type
28
29
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
4
6
  class TimeField < DatetimeField # :nodoc:
5
7
  private
6
-
7
8
  def format_date(value)
8
- value.try(:strftime, "%T.%L")
9
+ value&.strftime("%T.%L")
9
10
  end
10
11
  end
11
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -11,7 +13,7 @@ module ActionView
11
13
 
12
14
  def render
13
15
  select_content_tag(
14
- time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
16
+ time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
15
17
  )
16
18
  end
17
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -14,13 +16,8 @@ module ActionView
14
16
  translated_attribute || human_attribute_name
15
17
  end
16
18
 
17
- # TODO Change this to private once we've dropped Ruby 2.2 support.
18
- # Workaround for Ruby 2.2 "private attribute?" warning.
19
- protected
20
-
21
- attr_reader :object_name, :method_and_value, :scope, :model
22
-
23
19
  private
20
+ attr_reader :object_name, :method_and_value, :scope, :model
24
21
 
25
22
  def i18n_default
26
23
  if model
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module Helpers
3
5
  module Tags # :nodoc:
4
6
  class WeekField < DatetimeField # :nodoc:
5
7
  private
6
-
7
8
  def format_date(value)
8
- value.try(:strftime, "%Y-W%V")
9
+ value&.strftime("%Y-W%V")
9
10
  end
10
11
  end
11
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/string/filters"
2
4
  require "active_support/core_ext/array/extract_options"
3
5
 
@@ -11,9 +13,9 @@ module ActionView
11
13
  #
12
14
  # ==== Sanitization
13
15
  #
14
- # Most text helpers by default sanitize the given content, but do not escape it.
15
- # This means HTML tags will appear in the page but all malicious code will be removed.
16
- # Let's look at some examples using the +simple_format+ method:
16
+ # Most text helpers that generate HTML output sanitize the given input by default,
17
+ # but do not escape it. This means HTML tags will appear in the page but all malicious
18
+ # code will be removed. Let's look at some examples using the +simple_format+ method:
17
19
  #
18
20
  # simple_format('<a href="http://example.com/">Example</a>')
19
21
  # # => "<p><a href=\"http://example.com/\">Example</a></p>"
@@ -126,7 +128,7 @@ module ActionView
126
128
  # # => You searched for: <a href="search?q=rails">rails</a>
127
129
  #
128
130
  # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
129
- # # => "<a>ruby</a> on <mark>rails</mark>"
131
+ # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
130
132
  def highlight(text, phrases, options = {})
131
133
  text = sanitize(text) if options.fetch(:sanitize, true)
132
134
 
@@ -186,7 +188,7 @@ module ActionView
186
188
 
187
189
  unless separator.empty?
188
190
  text.split(separator).each do |value|
189
- if value.match(regex)
191
+ if value.match?(regex)
190
192
  phrase = value
191
193
  break
192
194
  end
@@ -226,7 +228,7 @@ module ActionView
226
228
  # pluralize(2, 'Person', locale: :de)
227
229
  # # => 2 Personen
228
230
  def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
229
- word = if (count == 1 || count =~ /^1(\.0+)?$/)
231
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
230
232
  singular
231
233
  else
232
234
  plural || singular.pluralize(locale)
@@ -257,7 +259,7 @@ module ActionView
257
259
  # # => Once\r\nupon\r\na\r\ntime
258
260
  def word_wrap(text, line_width: 80, break_sequence: "\n")
259
261
  text.split("\n").collect! do |line|
260
- 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
261
263
  end * break_sequence
262
264
  end
263
265
 
@@ -420,11 +422,10 @@ module ActionView
420
422
  def to_s
421
423
  value = @values[@index].to_s
422
424
  @index = next_index
423
- return value
425
+ value
424
426
  end
425
427
 
426
428
  private
427
-
428
429
  def next_index
429
430
  step_index(1)
430
431
  end
@@ -444,7 +445,7 @@ module ActionView
444
445
  # uses an instance variable of ActionView::Base.
445
446
  def get_cycle(name)
446
447
  @_cycles = Hash.new unless defined?(@_cycles)
447
- return @_cycles[name]
448
+ @_cycles[name]
448
449
  end
449
450
 
450
451
  def set_cycle(name, cycle_object)
@@ -1,18 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view/helpers/tag_helper"
2
- require "active_support/core_ext/string/access"
3
- require "i18n/exceptions"
4
+ require "active_support/core_ext/symbol/starts_ends_with"
4
5
 
5
6
  module ActionView
6
7
  # = Action View Translation Helpers
7
- module Helpers
8
+ module Helpers #:nodoc:
8
9
  module TranslationHelper
9
10
  extend ActiveSupport::Concern
10
11
 
11
12
  include TagHelper
12
13
 
13
14
  included do
14
- mattr_accessor :debug_missing_translation
15
- self.debug_missing_translation = true
15
+ mattr_accessor :debug_missing_translation, default: true
16
16
  end
17
17
 
18
18
  # Delegates to <tt>I18n#translate</tt> but also performs three additional
@@ -56,74 +56,85 @@ module ActionView
56
56
  # that include HTML tags so that you know what kind of output to expect
57
57
  # when you call translate in a template and translators know which keys
58
58
  # they can provide HTML values for.
59
- def translate(key, options = {})
60
- options = options.dup
61
- has_default = options.has_key?(:default)
62
- remaining_defaults = Array(options.delete(:default)).compact
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)
63
73
 
64
- if has_default && !remaining_defaults.first.kind_of?(Symbol)
65
- options[:default] = remaining_defaults
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
66
76
  end
67
77
 
68
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
69
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
70
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
71
- if options[:raise] == false
72
- raise_error = false
73
- i18n_raise = false
74
- else
75
- raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
76
- i18n_raise = true
77
- end
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
78
80
 
79
- if html_safe_translation_key?(key)
80
- html_safe_options = options.dup
81
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
82
- unless name == :count && value.is_a?(Numeric)
83
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
84
- end
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
85
84
  end
86
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
87
85
 
88
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
89
- else
90
- I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
91
- end
92
- rescue I18n::MissingTranslationData => e
93
- if remaining_defaults.present?
94
- translate remaining_defaults.shift, options.merge(default: remaining_defaults)
95
- else
96
- raise e if raise_error
97
-
98
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
99
- title = "translation missing: #{keys.join('.')}"
100
-
101
- interpolations = options.except(:default, :scope)
102
- if interpolations.any?
103
- title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
86
+ key = scope_key_by_partial(key)
87
+
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)
92
+ else
93
+ translated = I18n.translate(key, **options, default: default)
94
+ break translated unless translated.equal?(MISSING_TRANSLATION)
104
95
  end
105
96
 
106
- return title unless ActionView::Base.debug_missing_translation
97
+ break alternatives.first if alternatives.present? && !alternatives.first.is_a?(Symbol)
107
98
 
108
- content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
99
+ first_key ||= key
100
+ key = alternatives&.shift
109
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
110
109
  end
111
110
  alias :t :translate
112
111
 
113
112
  # Delegates to <tt>I18n.localize</tt> with no additional functionality.
114
113
  #
115
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
114
+ # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
116
115
  # for more information.
117
- def localize(*args)
118
- I18n.localize(*args)
116
+ def localize(object, **options)
117
+ I18n.localize(object, **options)
119
118
  end
120
119
  alias :l :localize
121
120
 
122
121
  private
122
+ MISSING_TRANSLATION = Object.new
123
+ private_constant :MISSING_TRANSLATION
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
+
123
132
  def scope_key_by_partial(key)
124
- if key.to_s.first == "."
133
+ if key&.start_with?(".")
125
134
  if @virtual_path
126
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
135
+ @_scope_key_by_partial_cache ||= {}
136
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
137
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
127
138
  else
128
139
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
129
140
  end
@@ -132,8 +143,47 @@ module ActionView
132
143
  end
133
144
  end
134
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
+
135
159
  def html_safe_translation_key?(key)
136
- /(\b|_|\.)html$/.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
137
187
  end
138
188
  end
139
189
  end