actionview 4.2.11.1 → 7.0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -215
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +116 -43
  6. data/lib/action_view/buffers.rb +20 -3
  7. data/lib/action_view/cache_expiry.rb +66 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  10. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  11. data/lib/action_view/dependency_tracker.rb +21 -122
  12. data/lib/action_view/digestor.rb +92 -85
  13. data/lib/action_view/flows.rb +15 -16
  14. data/lib/action_view/gem_version.rb +6 -4
  15. data/lib/action_view/helpers/active_model_helper.rb +17 -12
  16. data/lib/action_view/helpers/asset_tag_helper.rb +356 -101
  17. data/lib/action_view/helpers/asset_url_helper.rb +180 -74
  18. data/lib/action_view/helpers/atom_feed_helper.rb +21 -19
  19. data/lib/action_view/helpers/cache_helper.rb +156 -43
  20. data/lib/action_view/helpers/capture_helper.rb +21 -14
  21. data/lib/action_view/helpers/controller_helper.rb +16 -5
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  24. data/lib/action_view/helpers/date_helper.rb +288 -132
  25. data/lib/action_view/helpers/debug_helper.rb +9 -6
  26. data/lib/action_view/helpers/form_helper.rb +956 -173
  27. data/lib/action_view/helpers/form_options_helper.rb +178 -97
  28. data/lib/action_view/helpers/form_tag_helper.rb +220 -101
  29. data/lib/action_view/helpers/javascript_helper.rb +33 -19
  30. data/lib/action_view/helpers/number_helper.rb +88 -63
  31. data/lib/action_view/helpers/output_safety_helper.rb +38 -6
  32. data/lib/action_view/helpers/rendering_helper.rb +21 -10
  33. data/lib/action_view/helpers/sanitize_helper.rb +31 -32
  34. data/lib/action_view/helpers/tag_helper.rb +332 -71
  35. data/lib/action_view/helpers/tags/base.rb +123 -99
  36. data/lib/action_view/helpers/tags/check_box.rb +21 -20
  37. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  38. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  39. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  40. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  41. data/lib/action_view/helpers/tags/collection_select.rb +5 -3
  42. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  43. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  44. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  45. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  46. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  48. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  49. data/lib/action_view/helpers/tags/file_field.rb +18 -0
  50. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  51. data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
  52. data/lib/action_view/helpers/tags/label.rb +7 -2
  53. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  54. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  55. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  56. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  57. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  58. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  59. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  60. data/lib/action_view/helpers/tags/select.rb +11 -10
  61. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  62. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  63. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  64. data/lib/action_view/helpers/tags/time_field.rb +12 -2
  65. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  66. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  67. data/lib/action_view/helpers/tags/translator.rb +15 -16
  68. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  69. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  70. data/lib/action_view/helpers/tags/weekday_select.rb +28 -0
  71. data/lib/action_view/helpers/tags.rb +5 -2
  72. data/lib/action_view/helpers/text_helper.rb +80 -51
  73. data/lib/action_view/helpers/translation_helper.rb +120 -69
  74. data/lib/action_view/helpers/url_helper.rb +398 -171
  75. data/lib/action_view/helpers.rb +29 -27
  76. data/lib/action_view/layouts.rb +68 -63
  77. data/lib/action_view/log_subscriber.rb +77 -10
  78. data/lib/action_view/lookup_context.rb +137 -113
  79. data/lib/action_view/model_naming.rb +4 -2
  80. data/lib/action_view/path_set.rb +28 -32
  81. data/lib/action_view/railtie.rb +74 -13
  82. data/lib/action_view/record_identifier.rb +53 -26
  83. data/lib/action_view/render_parser.rb +188 -0
  84. data/lib/action_view/renderer/abstract_renderer.rb +152 -15
  85. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  86. data/lib/action_view/renderer/object_renderer.rb +34 -0
  87. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  88. data/lib/action_view/renderer/partial_renderer.rb +51 -333
  89. data/lib/action_view/renderer/renderer.rb +68 -11
  90. data/lib/action_view/renderer/streaming_template_renderer.rb +60 -56
  91. data/lib/action_view/renderer/template_renderer.rb +87 -74
  92. data/lib/action_view/rendering.rb +73 -47
  93. data/lib/action_view/ripper_ast_parser.rb +198 -0
  94. data/lib/action_view/routing_url_for.rb +35 -24
  95. data/lib/action_view/tasks/cache_digests.rake +25 -0
  96. data/lib/action_view/template/error.rb +151 -41
  97. data/lib/action_view/template/handlers/builder.rb +12 -13
  98. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  99. data/lib/action_view/template/handlers/erb.rb +29 -89
  100. data/lib/action_view/template/handlers/html.rb +11 -0
  101. data/lib/action_view/template/handlers/raw.rb +4 -4
  102. data/lib/action_view/template/handlers.rb +14 -10
  103. data/lib/action_view/template/html.rb +12 -13
  104. data/lib/action_view/template/inline.rb +22 -0
  105. data/lib/action_view/template/raw_file.rb +25 -0
  106. data/lib/action_view/template/renderable.rb +24 -0
  107. data/lib/action_view/template/resolver.rb +139 -300
  108. data/lib/action_view/template/sources/file.rb +17 -0
  109. data/lib/action_view/template/sources.rb +13 -0
  110. data/lib/action_view/template/text.rb +10 -12
  111. data/lib/action_view/template/types.rb +28 -26
  112. data/lib/action_view/template.rb +123 -91
  113. data/lib/action_view/template_details.rb +66 -0
  114. data/lib/action_view/template_path.rb +64 -0
  115. data/lib/action_view/test_case.rb +70 -53
  116. data/lib/action_view/testing/resolvers.rb +25 -35
  117. data/lib/action_view/unbound_template.rb +57 -0
  118. data/lib/action_view/version.rb +3 -1
  119. data/lib/action_view/view_paths.rb +73 -58
  120. data/lib/action_view.rb +16 -11
  121. data/lib/assets/compiled/rails-ujs.js +746 -0
  122. metadata +52 -32
  123. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  124. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,9 +1,15 @@
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/string/access"
5
+ require "active_support/core_ext/array/extract_options"
6
+ require "action_view/helpers/sanitize_helper"
7
+ require "action_view/helpers/tag_helper"
8
+ require "action_view/helpers/output_safety_helper"
3
9
 
4
10
  module ActionView
5
11
  # = Action View Text Helpers
6
- module Helpers #:nodoc:
12
+ module Helpers # :nodoc:
7
13
  # The TextHelper module provides a set of methods for filtering, formatting
8
14
  # and transforming strings, which can reduce the amount of inline Ruby code in
9
15
  # your views. These helper methods extend Action View making them callable
@@ -11,9 +17,9 @@ module ActionView
11
17
  #
12
18
  # ==== Sanitization
13
19
  #
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:
20
+ # Most text helpers that generate HTML output sanitize the given input by default,
21
+ # but do not escape it. This means HTML tags will appear in the page but all malicious
22
+ # code will be removed. Let's look at some examples using the +simple_format+ method:
17
23
  #
18
24
  # simple_format('<a href="http://example.com/">Example</a>')
19
25
  # # => "<p><a href=\"http://example.com/\">Example</a></p>"
@@ -103,7 +109,9 @@ module ActionView
103
109
  # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
104
110
  # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
105
111
  # 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.
112
+ # <tt><mark>\1</mark></tt>) or passing a block that receives each matched term. By default +text+
113
+ # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
114
+ # for <tt>:sanitize</tt> will turn sanitizing off.
107
115
  #
108
116
  # highlight('You searched for: rails', 'rails')
109
117
  # # => You searched for: <mark>rails</mark>
@@ -122,7 +130,10 @@ module ActionView
122
130
  #
123
131
  # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
124
132
  # # => You searched for: <a href="search?q=rails">rails</a>
125
- def highlight(text, phrases, options = {})
133
+ #
134
+ # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
135
+ # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
136
+ def highlight(text, phrases, options = {}, &block)
126
137
  text = sanitize(text) if options.fetch(:sanitize, true)
127
138
 
128
139
  if text.blank? || phrases.blank?
@@ -130,10 +141,10 @@ module ActionView
130
141
  else
131
142
  match = Array(phrases).map do |p|
132
143
  Regexp === p ? p.to_s : Regexp.escape(p)
133
- end.join('|')
144
+ end.join("|")
134
145
 
135
146
  if block_given?
136
- text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
147
+ text.gsub(/(#{match})(?![^<]*?>)/i, &block)
137
148
  else
138
149
  highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
139
150
  text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
@@ -146,7 +157,7 @@ module ActionView
146
157
  # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
147
158
  # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
148
159
  # <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.
160
+ # isn't found, +nil+ is returned.
150
161
  #
151
162
  # excerpt('This is an example', 'an', radius: 5)
152
163
  # # => ...s is an exam...
@@ -181,8 +192,8 @@ module ActionView
181
192
 
182
193
  unless separator.empty?
183
194
  text.split(separator).each do |value|
184
- if value.match(regex)
185
- regex = phrase = value
195
+ if value.match?(regex)
196
+ phrase = value
186
197
  break
187
198
  end
188
199
  end
@@ -199,7 +210,12 @@ module ActionView
199
210
 
200
211
  # Attempts to pluralize the +singular+ word unless +count+ is 1. If
201
212
  # +plural+ is supplied, it will use that when count is > 1, otherwise
202
- # it will use the Inflector to determine the plural form.
213
+ # it will use the Inflector to determine the plural form for the given locale,
214
+ # which defaults to I18n.locale
215
+ #
216
+ # The word will be pluralized using rules defined for the locale
217
+ # (you must define your own inflection rules for languages other than English).
218
+ # See ActiveSupport::Inflector.pluralize
203
219
  #
204
220
  # pluralize(1, 'person')
205
221
  # # => 1 person
@@ -207,16 +223,19 @@ module ActionView
207
223
  # pluralize(2, 'person')
208
224
  # # => 2 people
209
225
  #
210
- # pluralize(3, 'person', 'users')
226
+ # pluralize(3, 'person', plural: 'users')
211
227
  # # => 3 users
212
228
  #
213
229
  # pluralize(0, 'person')
214
230
  # # => 0 people
215
- def pluralize(count, singular, plural = nil)
216
- word = if (count == 1 || count =~ /^1(\.0+)?$/)
231
+ #
232
+ # pluralize(2, 'Person', locale: :de)
233
+ # # => 2 Personen
234
+ def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
235
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
217
236
  singular
218
237
  else
219
- plural || singular.pluralize
238
+ plural || singular.pluralize(locale)
220
239
  end
221
240
 
222
241
  "#{count || 0} #{word}"
@@ -237,19 +256,23 @@ module ActionView
237
256
  #
238
257
  # word_wrap('Once upon a time', line_width: 1)
239
258
  # # => Once\nupon\na\ntime
240
- def word_wrap(text, options = {})
241
- line_width = options.fetch(:line_width, 80)
242
-
259
+ #
260
+ # You can also specify a custom +break_sequence+ ("\n" by default)
261
+ #
262
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
263
+ # # => Once\r\nupon\r\na\r\ntime
264
+ def word_wrap(text, line_width: 80, break_sequence: "\n")
243
265
  text.split("\n").collect! do |line|
244
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
245
- end * "\n"
266
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
267
+ end * break_sequence
246
268
  end
247
269
 
248
270
  # 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+.
271
+ # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
272
+ # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
273
+ # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
274
+ # <tt><br /></tt> tag is appended. This method does not remove the
275
+ # newlines from the +text+.
253
276
  #
254
277
  # You can pass any HTML attributes into <tt>html_options</tt>. These
255
278
  # will be added to all created paragraphs.
@@ -309,7 +332,7 @@ module ActionView
309
332
  # <table>
310
333
  # <% @items.each do |item| %>
311
334
  # <tr class="<%= cycle("odd", "even") -%>">
312
- # <td>item</td>
335
+ # <td><%= item %></td>
313
336
  # </tr>
314
337
  # <% end %>
315
338
  # </table>
@@ -334,7 +357,7 @@ module ActionView
334
357
  # <% end %>
335
358
  def cycle(first_value, *values)
336
359
  options = values.extract_options!
337
- name = options.fetch(:name, 'default')
360
+ name = options.fetch(:name, "default")
338
361
 
339
362
  values.unshift(*first_value)
340
363
 
@@ -384,7 +407,7 @@ module ActionView
384
407
  cycle.reset if cycle
385
408
  end
386
409
 
387
- class Cycle #:nodoc:
410
+ class Cycle # :nodoc:
388
411
  attr_reader :values
389
412
 
390
413
  def initialize(first_value, *values)
@@ -403,22 +426,21 @@ module ActionView
403
426
  def to_s
404
427
  value = @values[@index].to_s
405
428
  @index = next_index
406
- return value
429
+ value
407
430
  end
408
431
 
409
432
  private
433
+ def next_index
434
+ step_index(1)
435
+ end
410
436
 
411
- def next_index
412
- step_index(1)
413
- end
414
-
415
- def previous_index
416
- step_index(-1)
417
- end
437
+ def previous_index
438
+ step_index(-1)
439
+ end
418
440
 
419
- def step_index(n)
420
- (@index + n) % @values.size
421
- end
441
+ def step_index(n)
442
+ (@index + n) % @values.size
443
+ end
422
444
  end
423
445
 
424
446
  private
@@ -427,7 +449,7 @@ module ActionView
427
449
  # uses an instance variable of ActionView::Base.
428
450
  def get_cycle(name)
429
451
  @_cycles = Hash.new unless defined?(@_cycles)
430
- return @_cycles[name]
452
+ @_cycles[name]
431
453
  end
432
454
 
433
455
  def set_cycle(name, cycle_object)
@@ -449,18 +471,25 @@ module ActionView
449
471
  radius = options.fetch(:radius, 100)
450
472
  omission = options.fetch(:omission, "...")
451
473
 
452
- part = part.split(separator)
453
- part.delete("")
454
- affix = part.size > radius ? omission : ""
474
+ if separator != ""
475
+ part = part.split(separator)
476
+ part.delete("")
477
+ end
455
478
 
456
- part = if part_position == :first
457
- drop_index = [part.length - radius, 0].max
458
- part.drop(drop_index)
459
- else
460
- part.first(radius)
479
+ affix = part.length > radius ? omission : ""
480
+
481
+ part =
482
+ if part_position == :first
483
+ part.last(radius)
484
+ else
485
+ part.first(radius)
486
+ end
487
+
488
+ if separator != ""
489
+ part = part.join(separator)
461
490
  end
462
491
 
463
- return affix, part.join(separator)
492
+ return affix, part
464
493
  end
465
494
  end
466
495
  end
@@ -1,99 +1,136 @@
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/html_safe_translation"
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
+ # Specify whether an error should be raised for missing translations
15
+ singleton_class.attr_accessor :raise_on_missing_translations
16
+
17
+ included do
18
+ mattr_accessor :debug_missing_translation, default: true
19
+ end
20
+
21
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional
22
+ # functions.
11
23
  #
12
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
13
- # into inline spans that:
24
+ # First, it will ensure that any thrown +MissingTranslation+ messages will
25
+ # be rendered as inline spans that:
14
26
  #
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.
27
+ # * Have a <tt>translation-missing</tt> class applied
28
+ # * Contain the missing key as the value of the +title+ attribute
29
+ # * Have a titleized version of the last key segment as text
18
30
  #
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.
31
+ # For example, the value returned for the missing translation key
32
+ # <tt>"blog.post.title"</tt> will be:
23
33
  #
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.
34
+ # <span
35
+ # class="translation_missing"
36
+ # title="translation missing: en.blog.post.title">Title</span>
31
37
  #
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
38
+ # This allows for views to display rather reasonable strings while still
39
+ # giving developers a way to find missing translations.
40
+ #
41
+ # If you would prefer missing translations to raise an error, you can
42
+ # opt out of span-wrapping behavior globally by setting
43
+ # <tt>config.i18n.raise_on_missing_translations = true</tt> or
44
+ # individually by passing <tt>raise: true</tt> as an option to
45
+ # <tt>translate</tt>.
46
+ #
47
+ # Second, if the key starts with a period <tt>translate</tt> will scope
48
+ # the key by the current partial. Calling <tt>translate(".foo")</tt> from
49
+ # the <tt>people/index.html.erb</tt> template is equivalent to calling
50
+ # <tt>translate("people.index.foo")</tt>. This makes it less
51
+ # repetitive to translate many keys within the same partial and provides
52
+ # a convention to scope keys consistently.
53
+ #
54
+ # Third, the translation will be marked as <tt>html_safe</tt> if the key
55
+ # has the suffix "_html" or the last element of the key is "html". Calling
56
+ # <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
57
+ # will return an HTML safe string that won't be escaped by other HTML
58
+ # helper methods. This naming convention helps to identify translations
59
+ # that include HTML tags so that you know what kind of output to expect
60
+ # when you call translate in a template and translators know which keys
61
+ # they can provide HTML values for.
62
+ #
63
+ # To access the translated text along with the fully resolved
64
+ # translation key, <tt>translate</tt> accepts a block:
65
+ #
66
+ # <%= translate(".relative_key") do |translation, resolved_key| %>
67
+ # <span title="<%= resolved_key %>"><%= translation %></span>
68
+ # <% end %>
69
+ #
70
+ # This enables annotate translated text to be aware of the scope it was
71
+ # resolved against.
72
+ #
73
+ def translate(key, **options)
74
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
75
+ key = key&.to_s unless key.is_a?(Symbol)
46
76
 
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
77
+ alternatives = if options.key?(:default)
78
+ options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
56
79
  end
57
80
 
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
81
+ options[:raise] = true if options[:raise].nil? && TranslationHelper.raise_on_missing_translations
82
+ default = MISSING_TRANSLATION
83
+
84
+ translation = while key || alternatives.present?
85
+ if alternatives.blank? && !options[:raise].nil?
86
+ default = NO_DEFAULT # let I18n handle missing translation
64
87
  end
65
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
66
88
 
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))
89
+ key = scope_key_by_partial(key)
90
+
91
+ translated = ActiveSupport::HtmlSafeTranslation.translate(key, **options, default: default)
92
+
93
+ break translated unless translated.equal?(MISSING_TRANSLATION)
94
+
95
+ if alternatives.present? && !alternatives.first.is_a?(Symbol)
96
+ break alternatives.first && I18n.translate(**options, default: alternatives)
97
+ end
98
+
99
+ first_key ||= key
100
+ key = alternatives&.shift
70
101
  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('.')}")
102
+
103
+ if key.nil? && !first_key.nil?
104
+ translation = missing_translation(first_key, options)
105
+ key = first_key
79
106
  end
107
+
108
+ block_given? ? yield(translation, key) : translation
80
109
  end
81
110
  alias :t :translate
82
111
 
83
112
  # Delegates to <tt>I18n.localize</tt> with no additional functionality.
84
113
  #
85
- # 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
86
115
  # for more information.
87
- def localize(*args)
88
- I18n.localize(*args)
116
+ def localize(object, **options)
117
+ I18n.localize(object, **options)
89
118
  end
90
119
  alias :l :localize
91
120
 
92
121
  private
122
+ MISSING_TRANSLATION = Object.new
123
+ private_constant :MISSING_TRANSLATION
124
+
125
+ NO_DEFAULT = [].freeze
126
+ private_constant :NO_DEFAULT
127
+
93
128
  def scope_key_by_partial(key)
94
- if key.to_s.first == "."
129
+ if key&.start_with?(".")
95
130
  if @virtual_path
96
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
131
+ @_scope_key_by_partial_cache ||= {}
132
+ @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".")
133
+ "#{@_scope_key_by_partial_cache[@virtual_path]}#{key}"
97
134
  else
98
135
  raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
99
136
  end
@@ -102,8 +139,22 @@ module ActionView
102
139
  end
103
140
  end
104
141
 
105
- def html_safe_translation_key?(key)
106
- key.to_s =~ /(\b|_|\.)html$/
142
+ def missing_translation(key, options)
143
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
144
+
145
+ title = +"translation missing: #{keys.join(".")}"
146
+
147
+ options.each do |name, value|
148
+ unless name == :scope
149
+ title << ", " << name.to_s << ": " << ERB::Util.html_escape(value)
150
+ end
151
+ end
152
+
153
+ if ActionView::Base.debug_missing_translation
154
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
155
+ else
156
+ title
157
+ end
107
158
  end
108
159
  end
109
160
  end