actionview 4.2.11.1 → 6.1.5

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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +232 -186
  3. data/MIT-LICENSE +1 -2
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +115 -39
  6. data/lib/action_view/buffers.rb +18 -1
  7. data/lib/action_view/cache_expiry.rb +52 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker.rb +61 -21
  10. data/lib/action_view/digestor.rb +89 -85
  11. data/lib/action_view/flows.rb +11 -12
  12. data/lib/action_view/gem_version.rb +6 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +16 -11
  14. data/lib/action_view/helpers/asset_tag_helper.rb +282 -83
  15. data/lib/action_view/helpers/asset_url_helper.rb +175 -69
  16. data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
  17. data/lib/action_view/helpers/cache_helper.rb +107 -43
  18. data/lib/action_view/helpers/capture_helper.rb +20 -13
  19. data/lib/action_view/helpers/controller_helper.rb +15 -4
  20. data/lib/action_view/helpers/csp_helper.rb +26 -0
  21. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  22. data/lib/action_view/helpers/date_helper.rb +232 -130
  23. data/lib/action_view/helpers/debug_helper.rb +7 -6
  24. data/lib/action_view/helpers/form_helper.rb +808 -146
  25. data/lib/action_view/helpers/form_options_helper.rb +124 -78
  26. data/lib/action_view/helpers/form_tag_helper.rb +120 -74
  27. data/lib/action_view/helpers/javascript_helper.rb +33 -17
  28. data/lib/action_view/helpers/number_helper.rb +87 -62
  29. data/lib/action_view/helpers/output_safety_helper.rb +36 -4
  30. data/lib/action_view/helpers/rendering_helper.rb +21 -10
  31. data/lib/action_view/helpers/sanitize_helper.rb +30 -31
  32. data/lib/action_view/helpers/tag_helper.rb +269 -68
  33. data/lib/action_view/helpers/tags/base.rb +141 -97
  34. data/lib/action_view/helpers/tags/check_box.rb +20 -19
  35. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  37. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  38. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  39. data/lib/action_view/helpers/tags/collection_select.rb +4 -2
  40. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  41. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  42. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  43. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  47. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  48. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  49. data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
  50. data/lib/action_view/helpers/tags/label.rb +7 -2
  51. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  52. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  54. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  55. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  56. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  58. data/lib/action_view/helpers/tags/select.rb +11 -10
  59. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  61. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  62. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  63. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  64. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  65. data/lib/action_view/helpers/tags/translator.rb +15 -16
  66. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  67. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  68. data/lib/action_view/helpers/tags.rb +3 -1
  69. data/lib/action_view/helpers/text_helper.rb +56 -38
  70. data/lib/action_view/helpers/translation_helper.rb +150 -68
  71. data/lib/action_view/helpers/url_helper.rb +284 -117
  72. data/lib/action_view/helpers.rb +5 -3
  73. data/lib/action_view/layouts.rb +68 -63
  74. data/lib/action_view/log_subscriber.rb +77 -10
  75. data/lib/action_view/lookup_context.rb +134 -91
  76. data/lib/action_view/model_naming.rb +3 -1
  77. data/lib/action_view/path_set.rb +26 -24
  78. data/lib/action_view/railtie.rb +62 -13
  79. data/lib/action_view/record_identifier.rb +53 -26
  80. data/lib/action_view/renderer/abstract_renderer.rb +151 -14
  81. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  82. data/lib/action_view/renderer/object_renderer.rb +34 -0
  83. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +55 -303
  85. data/lib/action_view/renderer/renderer.rb +66 -9
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
  87. data/lib/action_view/renderer/template_renderer.rb +82 -73
  88. data/lib/action_view/rendering.rb +71 -45
  89. data/lib/action_view/routing_url_for.rb +34 -23
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template/error.rb +44 -29
  92. data/lib/action_view/template/handlers/builder.rb +12 -13
  93. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  94. data/lib/action_view/template/handlers/erb.rb +23 -89
  95. data/lib/action_view/template/handlers/html.rb +11 -0
  96. data/lib/action_view/template/handlers/raw.rb +4 -4
  97. data/lib/action_view/template/handlers.rb +12 -8
  98. data/lib/action_view/template/html.rb +10 -11
  99. data/lib/action_view/template/inline.rb +22 -0
  100. data/lib/action_view/template/raw_file.rb +25 -0
  101. data/lib/action_view/template/renderable.rb +24 -0
  102. data/lib/action_view/template/resolver.rb +263 -197
  103. data/lib/action_view/template/sources/file.rb +17 -0
  104. data/lib/action_view/template/sources.rb +13 -0
  105. data/lib/action_view/template/text.rb +8 -10
  106. data/lib/action_view/template/types.rb +18 -18
  107. data/lib/action_view/template.rb +108 -92
  108. data/lib/action_view/test_case.rb +66 -53
  109. data/lib/action_view/testing/resolvers.rb +24 -33
  110. data/lib/action_view/unbound_template.rb +31 -0
  111. data/lib/action_view/version.rb +3 -1
  112. data/lib/action_view/view_paths.rb +73 -58
  113. data/lib/action_view.rb +14 -8
  114. data/lib/assets/compiled/rails-ujs.js +746 -0
  115. metadata +42 -29
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  117. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,5 +1,7 @@
1
- require 'active_support/core_ext/string/filters'
2
- require 'active_support/core_ext/array/extract_options'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/filters"
4
+ require "active_support/core_ext/array/extract_options"
3
5
 
4
6
  module ActionView
5
7
  # = Action View Text Helpers
@@ -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>"
@@ -103,7 +105,9 @@ module ActionView
103
105
  # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
104
106
  # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
105
107
  # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
106
- # '<mark>\1</mark>') or passing a block that receives each matched term.
108
+ # <tt><mark>\1</mark></tt>) or passing a block that receives each matched term. By default +text+
109
+ # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
110
+ # for <tt>:sanitize</tt> will turn sanitizing off.
107
111
  #
108
112
  # highlight('You searched for: rails', 'rails')
109
113
  # # => You searched for: <mark>rails</mark>
@@ -122,6 +126,9 @@ module ActionView
122
126
  #
123
127
  # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
124
128
  # # => You searched for: <a href="search?q=rails">rails</a>
129
+ #
130
+ # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
131
+ # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
125
132
  def highlight(text, phrases, options = {})
126
133
  text = sanitize(text) if options.fetch(:sanitize, true)
127
134
 
@@ -130,7 +137,7 @@ module ActionView
130
137
  else
131
138
  match = Array(phrases).map do |p|
132
139
  Regexp === p ? p.to_s : Regexp.escape(p)
133
- end.join('|')
140
+ end.join("|")
134
141
 
135
142
  if block_given?
136
143
  text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
@@ -146,7 +153,7 @@ module ActionView
146
153
  # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
147
154
  # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
148
155
  # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
149
- # isn't found, nil is returned.
156
+ # isn't found, +nil+ is returned.
150
157
  #
151
158
  # excerpt('This is an example', 'an', radius: 5)
152
159
  # # => ...s is an exam...
@@ -181,8 +188,8 @@ module ActionView
181
188
 
182
189
  unless separator.empty?
183
190
  text.split(separator).each do |value|
184
- if value.match(regex)
185
- regex = phrase = value
191
+ if value.match?(regex)
192
+ phrase = value
186
193
  break
187
194
  end
188
195
  end
@@ -199,7 +206,12 @@ module ActionView
199
206
 
200
207
  # Attempts to pluralize the +singular+ word unless +count+ is 1. If
201
208
  # +plural+ is supplied, it will use that when count is > 1, otherwise
202
- # it will use the Inflector to determine the plural form.
209
+ # it will use the Inflector to determine the plural form for the given locale,
210
+ # which defaults to I18n.locale
211
+ #
212
+ # The word will be pluralized using rules defined for the locale
213
+ # (you must define your own inflection rules for languages other than English).
214
+ # See ActiveSupport::Inflector.pluralize
203
215
  #
204
216
  # pluralize(1, 'person')
205
217
  # # => 1 person
@@ -207,16 +219,19 @@ module ActionView
207
219
  # pluralize(2, 'person')
208
220
  # # => 2 people
209
221
  #
210
- # pluralize(3, 'person', 'users')
222
+ # pluralize(3, 'person', plural: 'users')
211
223
  # # => 3 users
212
224
  #
213
225
  # pluralize(0, 'person')
214
226
  # # => 0 people
215
- def pluralize(count, singular, plural = nil)
216
- word = if (count == 1 || count =~ /^1(\.0+)?$/)
227
+ #
228
+ # pluralize(2, 'Person', locale: :de)
229
+ # # => 2 Personen
230
+ def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
231
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
217
232
  singular
218
233
  else
219
- plural || singular.pluralize
234
+ plural || singular.pluralize(locale)
220
235
  end
221
236
 
222
237
  "#{count || 0} #{word}"
@@ -237,19 +252,23 @@ module ActionView
237
252
  #
238
253
  # word_wrap('Once upon a time', line_width: 1)
239
254
  # # => Once\nupon\na\ntime
240
- def word_wrap(text, options = {})
241
- line_width = options.fetch(:line_width, 80)
242
-
255
+ #
256
+ # You can also specify a custom +break_sequence+ ("\n" by default)
257
+ #
258
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
259
+ # # => Once\r\nupon\r\na\r\ntime
260
+ def word_wrap(text, line_width: 80, break_sequence: "\n")
243
261
  text.split("\n").collect! do |line|
244
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
245
- end * "\n"
262
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
263
+ end * break_sequence
246
264
  end
247
265
 
248
266
  # Returns +text+ transformed into HTML using simple formatting rules.
249
- # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
250
- # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
251
- # considered as a linebreak and a <tt><br /></tt> tag is appended. This
252
- # method does not remove the newlines from the +text+.
267
+ # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
268
+ # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
269
+ # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
270
+ # <tt><br /></tt> tag is appended. This method does not remove the
271
+ # newlines from the +text+.
253
272
  #
254
273
  # You can pass any HTML attributes into <tt>html_options</tt>. These
255
274
  # will be added to all created paragraphs.
@@ -309,7 +328,7 @@ module ActionView
309
328
  # <table>
310
329
  # <% @items.each do |item| %>
311
330
  # <tr class="<%= cycle("odd", "even") -%>">
312
- # <td>item</td>
331
+ # <td><%= item %></td>
313
332
  # </tr>
314
333
  # <% end %>
315
334
  # </table>
@@ -334,7 +353,7 @@ module ActionView
334
353
  # <% end %>
335
354
  def cycle(first_value, *values)
336
355
  options = values.extract_options!
337
- name = options.fetch(:name, 'default')
356
+ name = options.fetch(:name, "default")
338
357
 
339
358
  values.unshift(*first_value)
340
359
 
@@ -403,22 +422,21 @@ module ActionView
403
422
  def to_s
404
423
  value = @values[@index].to_s
405
424
  @index = next_index
406
- return value
425
+ value
407
426
  end
408
427
 
409
428
  private
429
+ def next_index
430
+ step_index(1)
431
+ end
410
432
 
411
- def next_index
412
- step_index(1)
413
- end
414
-
415
- def previous_index
416
- step_index(-1)
417
- end
433
+ def previous_index
434
+ step_index(-1)
435
+ end
418
436
 
419
- def step_index(n)
420
- (@index + n) % @values.size
421
- end
437
+ def step_index(n)
438
+ (@index + n) % @values.size
439
+ end
422
440
  end
423
441
 
424
442
  private
@@ -427,7 +445,7 @@ module ActionView
427
445
  # uses an instance variable of ActionView::Base.
428
446
  def get_cycle(name)
429
447
  @_cycles = Hash.new unless defined?(@_cycles)
430
- return @_cycles[name]
448
+ @_cycles[name]
431
449
  end
432
450
 
433
451
  def set_cycle(name, cycle_object)
@@ -1,99 +1,142 @@
1
- require 'action_view/helpers/tag_helper'
2
- require 'active_support/core_ext/string/access'
3
- require 'i18n/exceptions'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/tag_helper"
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
10
+ extend ActiveSupport::Concern
11
+
9
12
  include TagHelper
10
- # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
13
+
14
+ included do
15
+ mattr_accessor :debug_missing_translation, default: true
16
+ end
17
+
18
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional
19
+ # functions.
11
20
  #
12
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
13
- # into inline spans that:
21
+ # First, it will ensure that any thrown +MissingTranslation+ messages will
22
+ # be rendered as inline spans that:
14
23
  #
15
- # * have a "translation-missing" class set,
16
- # * contain the missing key as a title attribute and
17
- # * a titleized version of the last key segment as a text.
24
+ # * Have a <tt>translation-missing</tt> class applied
25
+ # * Contain the missing key as the value of the +title+ attribute
26
+ # * Have a titleized version of the last key segment as text
18
27
  #
19
- # E.g. the value returned for a missing translation key :"blog.post.title" will be
20
- # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>.
21
- # This way your views will display rather reasonable strings but it will still
22
- # be easy to spot missing translations.
28
+ # For example, the value returned for the missing translation key
29
+ # <tt>"blog.post.title"</tt> will be:
23
30
  #
24
- # Second, it'll scope the key by the current partial if the key starts
25
- # with a period. So if you call <tt>translate(".foo")</tt> from the
26
- # <tt>people/index.html.erb</tt> template, you'll actually be calling
27
- # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
28
- # to translate many keys within the same partials and gives you a simple framework
29
- # for scoping them consistently. If you don't prepend the key with a period,
30
- # nothing is converted.
31
+ # <span
32
+ # class="translation_missing"
33
+ # title="translation missing: en.blog.post.title">Title</span>
31
34
  #
32
- # Third, it'll mark the translation as safe HTML if the key has the suffix
33
- # "_html" or the last element of the key is the word "html". For example,
34
- # calling translate("footer_html") or translate("footer.html") will return
35
- # a safe HTML string that won't be escaped by other HTML helper methods. This
36
- # naming convention helps to identify translations that include HTML tags so that
37
- # you know what kind of output to expect when you call translate in a template.
38
- def translate(key, options = {})
39
- options = options.dup
40
- has_default = options.has_key?(:default)
41
- remaining_defaults = Array(options.delete(:default)).compact
42
-
43
- if has_default && !remaining_defaults.first.kind_of?(Symbol)
44
- options[:default] = remaining_defaults
45
- end
35
+ # This allows for views to display rather reasonable strings while still
36
+ # giving developers a way to find missing translations.
37
+ #
38
+ # If you would prefer missing translations to raise an error, you can
39
+ # opt out of span-wrapping behavior globally by setting
40
+ # <tt>ActionView::Base.raise_on_missing_translations = true</tt> or
41
+ # individually by passing <tt>raise: true</tt> as an option to
42
+ # <tt>translate</tt>.
43
+ #
44
+ # Second, if the key starts with a period <tt>translate</tt> will scope
45
+ # the key by the current partial. Calling <tt>translate(".foo")</tt> from
46
+ # the <tt>people/index.html.erb</tt> template is equivalent to calling
47
+ # <tt>translate("people.index.foo")</tt>. This makes it less
48
+ # repetitive to translate many keys within the same partial and provides
49
+ # a convention to scope keys consistently.
50
+ #
51
+ # Third, the translation will be marked as <tt>html_safe</tt> if the key
52
+ # has the suffix "_html" or the last element of the key is "html". Calling
53
+ # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
54
+ # will return an HTML safe string that won't be escaped by other HTML
55
+ # helper methods. This naming convention helps to identify translations
56
+ # that include HTML tags so that you know what kind of output to expect
57
+ # when you call translate in a template and translators know which keys
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
+ #
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)
46
73
 
47
- # If the user has explicitly decided to NOT raise errors, pass that option to I18n.
48
- # Otherwise, tell I18n to raise an exception, which we rescue further in this method.
49
- # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
50
- if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?)
51
- raise_error = false
52
- i18n_raise = false
53
- else
54
- raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations
55
- i18n_raise = true
74
+ alternatives = if options.key?(:default)
75
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
56
76
  end
57
77
 
58
- if html_safe_translation_key?(key)
59
- html_safe_options = options.dup
60
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
61
- unless name == :count && value.is_a?(Numeric)
62
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
63
- end
78
+ options[:raise] = true if options[:raise].nil? && ActionView::Base.raise_on_missing_translations
79
+ default = MISSING_TRANSLATION
80
+
81
+ translation = while key || alternatives.present?
82
+ if alternatives.blank? && !options[:raise].nil?
83
+ default = NO_DEFAULT # let I18n handle missing translation
84
+ end
85
+
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)
95
+ end
96
+
97
+ if alternatives.present? && !alternatives.first.is_a?(Symbol)
98
+ break alternatives.first && I18n.translate(**options, default: alternatives)
64
99
  end
65
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
66
100
 
67
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
68
- else
69
- I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
101
+ first_key ||= key
102
+ key = alternatives&.shift
70
103
  end
71
- rescue I18n::MissingTranslationData => e
72
- if remaining_defaults.present?
73
- translate remaining_defaults.shift, options.merge(default: remaining_defaults)
74
- else
75
- raise e if raise_error
76
-
77
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
78
- content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
104
+
105
+ if key.nil? && !first_key.nil?
106
+ translation = missing_translation(first_key, options)
107
+ key = first_key
79
108
  end
109
+
110
+ block_given? ? yield(translation, key) : translation
80
111
  end
81
112
  alias :t :translate
82
113
 
83
114
  # Delegates to <tt>I18n.localize</tt> with no additional functionality.
84
115
  #
85
- # 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
86
117
  # for more information.
87
- def localize(*args)
88
- I18n.localize(*args)
118
+ def localize(object, **options)
119
+ I18n.localize(object, **options)
89
120
  end
90
121
  alias :l :localize
91
122
 
92
123
  private
124
+ MISSING_TRANSLATION = Object.new
125
+ private_constant :MISSING_TRANSLATION
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
+
93
134
  def scope_key_by_partial(key)
94
- if key.to_s.first == "."
135
+ if key&.start_with?(".")
95
136
  if @virtual_path
96
- @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}"
97
140
  else
98
141
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
99
142
  end
@@ -102,8 +145,47 @@ module ActionView
102
145
  end
103
146
  end
104
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
+
105
161
  def html_safe_translation_key?(key)
106
- key.to_s =~ /(\b|_|\.)html$/
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
107
189
  end
108
190
  end
109
191
  end