i18n-tasks 1.0.15 → 1.1.0

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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -13
  3. data/Rakefile +4 -4
  4. data/bin/i18n-tasks +3 -3
  5. data/config/locales/en.yml +6 -0
  6. data/config/locales/ru.yml +7 -0
  7. data/i18n-tasks.gemspec +28 -41
  8. data/lib/i18n/tasks/base_task.rb +19 -19
  9. data/lib/i18n/tasks/cli.rb +37 -30
  10. data/lib/i18n/tasks/command/collection.rb +4 -4
  11. data/lib/i18n/tasks/command/commander.rb +5 -5
  12. data/lib/i18n/tasks/command/commands/check_prism.rb +126 -0
  13. data/lib/i18n/tasks/command/commands/data.rb +33 -33
  14. data/lib/i18n/tasks/command/commands/eq_base.rb +3 -3
  15. data/lib/i18n/tasks/command/commands/health.rb +6 -5
  16. data/lib/i18n/tasks/command/commands/interpolations.rb +14 -3
  17. data/lib/i18n/tasks/command/commands/meta.rb +6 -6
  18. data/lib/i18n/tasks/command/commands/missing.rb +25 -25
  19. data/lib/i18n/tasks/command/commands/tree.rb +33 -33
  20. data/lib/i18n/tasks/command/commands/usages.rb +24 -24
  21. data/lib/i18n/tasks/command/dsl.rb +1 -1
  22. data/lib/i18n/tasks/command/option_parsers/enum.rb +5 -5
  23. data/lib/i18n/tasks/command/option_parsers/locale.rb +4 -4
  24. data/lib/i18n/tasks/command/options/common.rb +16 -16
  25. data/lib/i18n/tasks/command/options/data.rb +18 -18
  26. data/lib/i18n/tasks/command/options/locales.rb +32 -32
  27. data/lib/i18n/tasks/commands.rb +14 -12
  28. data/lib/i18n/tasks/concurrent/cache.rb +1 -1
  29. data/lib/i18n/tasks/concurrent/cached_value.rb +1 -1
  30. data/lib/i18n/tasks/configuration.rb +22 -21
  31. data/lib/i18n/tasks/console_context.rb +11 -11
  32. data/lib/i18n/tasks/data/adapter/json_adapter.rb +1 -1
  33. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +5 -5
  34. data/lib/i18n/tasks/data/file_formats.rb +3 -3
  35. data/lib/i18n/tasks/data/file_system.rb +5 -5
  36. data/lib/i18n/tasks/data/file_system_base.rb +26 -26
  37. data/lib/i18n/tasks/data/language_names.rb +202 -0
  38. data/lib/i18n/tasks/data/router/conservative_router.rb +3 -3
  39. data/lib/i18n/tasks/data/router/isolating_router.rb +19 -19
  40. data/lib/i18n/tasks/data/router/pattern_router.rb +5 -5
  41. data/lib/i18n/tasks/data/tree/node.rb +27 -27
  42. data/lib/i18n/tasks/data/tree/nodes.rb +10 -10
  43. data/lib/i18n/tasks/data/tree/siblings.rb +20 -20
  44. data/lib/i18n/tasks/data/tree/traversal.rb +5 -5
  45. data/lib/i18n/tasks/data.rb +4 -4
  46. data/lib/i18n/tasks/html_keys.rb +2 -2
  47. data/lib/i18n/tasks/ignore_keys.rb +9 -9
  48. data/lib/i18n/tasks/interpolations.rb +21 -1
  49. data/lib/i18n/tasks/key_pattern_matching.rb +8 -8
  50. data/lib/i18n/tasks/logging.rb +2 -1
  51. data/lib/i18n/tasks/missing_keys.rb +24 -8
  52. data/lib/i18n/tasks/plural_keys.rb +6 -4
  53. data/lib/i18n/tasks/references.rb +4 -4
  54. data/lib/i18n/tasks/reports/base.rb +18 -14
  55. data/lib/i18n/tasks/reports/terminal.rb +64 -47
  56. data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +3 -3
  57. data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +3 -3
  58. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +10 -10
  59. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +1 -1
  60. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +69 -10
  61. data/lib/i18n/tasks/scanners/file_scanner.rb +5 -5
  62. data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +3 -3
  63. data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +3 -3
  64. data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +2 -2
  65. data/lib/i18n/tasks/scanners/files/file_finder.rb +8 -8
  66. data/lib/i18n/tasks/scanners/files/file_reader.rb +1 -1
  67. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +8 -8
  68. data/lib/i18n/tasks/scanners/occurrence_from_position.rb +1 -1
  69. data/lib/i18n/tasks/scanners/pattern_mapper.rb +7 -7
  70. data/lib/i18n/tasks/scanners/pattern_scanner.rb +20 -20
  71. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +8 -8
  72. data/lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb +8 -1
  73. data/lib/i18n/tasks/scanners/prism_scanners/nodes.rb +101 -61
  74. data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +169 -105
  75. data/lib/i18n/tasks/scanners/relative_keys.rb +8 -8
  76. data/lib/i18n/tasks/scanners/results/key_occurrences.rb +3 -3
  77. data/lib/i18n/tasks/scanners/results/occurrence.rb +14 -10
  78. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +1 -1
  79. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +6 -6
  80. data/lib/i18n/tasks/scanners/ruby_parser_factory.rb +1 -1
  81. data/lib/i18n/tasks/scanners/ruby_scanner.rb +225 -0
  82. data/lib/i18n/tasks/scanners/scanner.rb +2 -2
  83. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +1 -1
  84. data/lib/i18n/tasks/split_key.rb +4 -4
  85. data/lib/i18n/tasks/stats.rb +3 -3
  86. data/lib/i18n/tasks/translation.rb +5 -5
  87. data/lib/i18n/tasks/translators/base_translator.rb +40 -14
  88. data/lib/i18n/tasks/translators/deepl_translator.rb +17 -14
  89. data/lib/i18n/tasks/translators/google_translator.rb +169 -25
  90. data/lib/i18n/tasks/translators/openai_translator.rb +34 -23
  91. data/lib/i18n/tasks/translators/watsonx_translator.rb +16 -16
  92. data/lib/i18n/tasks/translators/yandex_translator.rb +8 -8
  93. data/lib/i18n/tasks/unused_keys.rb +1 -1
  94. data/lib/i18n/tasks/used_keys.rb +32 -33
  95. data/lib/i18n/tasks/version.rb +1 -1
  96. data/lib/i18n/tasks.rb +17 -17
  97. data/templates/config/i18n-tasks.yml +12 -0
  98. data/templates/minitest/i18n_test.rb +3 -3
  99. data/templates/rspec/i18n_spec.rb +7 -7
  100. metadata +25 -185
  101. data/lib/i18n/tasks/scanners/prism_scanner.rb +0 -83
  102. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +0 -145
@@ -1,29 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'prism/visitor'
4
- require_relative 'nodes'
5
- require_relative 'arguments_visitor'
3
+ require "prism/visitor"
4
+ require_relative "nodes"
5
+ require_relative "arguments_visitor"
6
6
 
7
7
  # Implementation of Prism::Visitor (https://ruby.github.io/prism/rb/Prism/Visitor.html)
8
8
  # It processes the parsed AST from Prism and creates a new AST with the nodes defined in prism_scanners/nodes.rb
9
- # The only argument it receives is comments, which can be used for magic comments.
10
9
  # It defines processing of arguments in a way that is needed for the translation calls.
11
- # Any Rails-specific processing is added in the RailsVisitor class.
10
+ # The argument `rails` is used to determine if the scanner should handle Rails specific calls, such as
11
+ # `before_action`, `human_attribute_name`, and `model_name.human`.
12
12
 
13
13
  module I18n::Tasks::Scanners::PrismScanners
14
14
  class Visitor < Prism::Visitor # rubocop:disable Metrics/ClassLength
15
- MAGIC_COMMENT_PREFIX = /\A.\s*i18n-tasks-use\s+/.freeze
15
+ MAGIC_COMMENT_PREFIX = /\A.\s*i18n-tasks-use\s+/
16
16
 
17
17
  attr_reader(:calls, :current_module, :current_class, :current_method, :root)
18
18
 
19
- def initialize(comments: nil, rails: false)
19
+ def initialize(rails: false, file_path: nil)
20
20
  @calls = []
21
- @comment_translations_by_row = prepare_comments_by_line(comments)
22
21
 
23
22
  @current_module = nil
24
23
  @current_class = nil
25
24
  @current_method = nil
26
- @root = Root.new
25
+ @root = Root.new(file_path:, rails: rails)
27
26
 
28
27
  @rails = rails
29
28
 
@@ -35,47 +34,24 @@ module I18n::Tasks::Scanners::PrismScanners
35
34
  @current_before_action || @current_method || @current_class || @current_module || @root
36
35
  end
37
36
 
38
- def prepare_comments_by_line(comments)
39
- return {} if comments.nil?
40
-
41
- comments.each_with_object({}) do |comment, by_row|
42
- content =
43
- comment.respond_to?(:slice) ? comment.slice : comment.location.slice
44
- match = content.match(MAGIC_COMMENT_PREFIX)
45
-
46
- next by_row if match.nil?
47
-
48
- string =
49
- content.gsub(MAGIC_COMMENT_PREFIX, '').gsub('#', '').strip
50
- visitor = Visitor.new
51
- Prism
52
- .parse(string)
53
- .value
54
- .accept(visitor)
55
-
56
- nodes = visitor.process
57
-
58
- next by_row if nodes.empty?
59
-
60
- # Remap the found translation calls to be for the found comment
61
- nodes = nodes.map { |node| node.with_node(comment) }
62
-
63
- by_row[comment.location.start_line] = nodes
64
- by_row
65
- end
66
- end
67
-
68
37
  def visit_module_node(node)
69
38
  previous_module = @current_module
70
39
  @current_module = parent.add_child(
71
40
  ParsedModule.new(node: node, parent: parent)
72
41
  )
73
42
 
43
+ handle_comments(node)
44
+
74
45
  super
75
46
  ensure
76
47
  @current_module = previous_module
77
48
  end
78
49
 
50
+ def visit_program_node(node)
51
+ handle_comments(node)
52
+ super
53
+ end
54
+
79
55
  def visit_class_node(node)
80
56
  previous_class = @current_class
81
57
 
@@ -87,12 +63,15 @@ module I18n::Tasks::Scanners::PrismScanners
87
63
  )
88
64
  )
89
65
 
66
+ handle_comments(node)
67
+
90
68
  super
91
69
  ensure
92
70
  @current_class = previous_class
93
71
  end
94
72
 
95
73
  def visit_def_node(node)
74
+ handle_comments(node)
96
75
  previous_method = @current_method
97
76
  parent = @current_class || @current_module || @root
98
77
  @current_method = parent.add_child(
@@ -115,7 +94,6 @@ module I18n::Tasks::Scanners::PrismScanners
115
94
  when :private
116
95
  @current_class&.private_methods!
117
96
  when :t, :t!, :translate, :translate!
118
-
119
97
  args, kwargs = process_arguments(node)
120
98
  parent.add_translation_call(
121
99
  TranslationCall.new(
@@ -127,24 +105,18 @@ module I18n::Tasks::Scanners::PrismScanners
127
105
  )
128
106
  )
129
107
  else
130
- if @rails
131
- return rails_call_node(node) do
132
- super
133
- end || parent.add_call(node)
134
- else
135
- parent.add_call(node)
136
- end
108
+ handled_call = handle_rails_call_node(node) { super } if @rails
109
+
110
+ return if handled_call
111
+
112
+ parent.add_call(node)
113
+
137
114
  end
138
115
 
139
116
  super
140
117
  end
141
118
 
142
119
  def process
143
- @comment_translations_by_row.each_value do |nodes|
144
- nodes.each do |node|
145
- @root.add_translation_call(node)
146
- end
147
- end
148
120
  @root.process
149
121
  end
150
122
 
@@ -163,27 +135,94 @@ module I18n::Tasks::Scanners::PrismScanners
163
135
  end
164
136
 
165
137
  def handle_comments(node)
166
- comments = @comment_translations_by_row[node.location.start_line - 1]
167
- comments&.each do |comment|
168
- parent.add_translation_call(comment.with_node(node))
169
- @comment_translations_by_row.delete(node.location.start_line - 1)
138
+ return if node.nil?
139
+ return if node.comments.empty?
140
+
141
+ node.comments.each do |comment|
142
+ content =
143
+ comment.respond_to?(:slice) ? comment.slice : comment.location.slice
144
+ match = content.match(MAGIC_COMMENT_PREFIX)
145
+
146
+ next if match.nil?
147
+
148
+ string =
149
+ content.gsub(MAGIC_COMMENT_PREFIX, "").delete("#").strip
150
+ visitor = Visitor.new
151
+ Prism
152
+ .parse(string)
153
+ .value
154
+ .accept(visitor)
155
+
156
+ # Process and remap the found translation calls to be for the found comment
157
+ visitor.process.each do |comment_node|
158
+ parent.add_translation_call(comment_node.with_node(node))
159
+ end
170
160
  end
171
161
  end
172
162
 
173
163
  # ---- Rails specific methods ----
174
164
  # Returns true if the node was handled
175
- def rails_call_node(node, &block)
165
+ def handle_rails_call_node(node, &)
166
+ # First, handle calls that should be matched everywhere
176
167
  case node.name
177
- when :before_action
178
- rails_handle_before_action(node, &block)
179
- true
180
168
  when :human_attribute_name
181
169
  rails_handle_human_attribute_name(node)
182
- true
170
+ return true
183
171
  when :human
184
- return false if node.receiver.name != :model_name
185
-
172
+ return false if node.receiver.nil? || node.receiver.name != :model_name
186
173
  rails_handle_model_name(node)
174
+ return true
175
+ end
176
+
177
+ if @current_class&.controller?
178
+ rails_handle_controller_call_node(node, &)
179
+ elsif @current_class&.mailer?
180
+ rails_handle_mailer_call_node(node, &)
181
+ else
182
+ false
183
+ end
184
+ end
185
+
186
+ def rails_handle_controller_call_node(node, &)
187
+ case node.name
188
+ when :before_action
189
+ rails_handle_before_action(node, &)
190
+ true
191
+ else
192
+ false
193
+ end
194
+ end
195
+
196
+ def rails_handle_mailer_call_node(node, &)
197
+ case node.name
198
+ when :mail
199
+ _array_args, keywords = process_arguments(node)
200
+ if keywords.key?("subject")
201
+ # Let traversal continue so nested calls (e.g., t('.subject') or default_i18n_subject)
202
+ # get processed by their own handlers.
203
+ false
204
+ else
205
+ parent.add_translation_call(
206
+ TranslationCall.new(
207
+ node: node,
208
+ receiver: nil,
209
+ key: ".subject",
210
+ parent: parent,
211
+ options: {}
212
+ )
213
+ )
214
+ true
215
+ end
216
+ when :default_i18n_subject
217
+ parent.add_translation_call(
218
+ TranslationCall.new(
219
+ node: node,
220
+ receiver: nil,
221
+ key: ".subject",
222
+ parent: parent,
223
+ options: {}
224
+ )
225
+ )
187
226
  true
188
227
  else
189
228
  false
@@ -191,36 +230,32 @@ module I18n::Tasks::Scanners::PrismScanners
191
230
  end
192
231
 
193
232
  def rails_handle_before_action(node) # rubocop:disable Metrics/MethodLength
233
+ before_action = ParsedBeforeAction.new(
234
+ node: node,
235
+ parent: @current_class
236
+ )
237
+ @current_class&.add_child(before_action)
238
+ # Need to set current_before_action before processing arguments
239
+ # since they can be lambdas that need to be processed in the context of the before_action
240
+ @current_before_action = before_action
194
241
  array_arguments, keywords = process_arguments(node)
195
242
  first_argument = array_arguments.first
196
243
 
197
- before_action = if array_arguments.empty? && node.block.present?
198
- ParsedBeforeAction.new(
199
- node: node,
200
- parent: parent
201
- )
202
- elsif first_argument.is_a?(String)
203
- ParsedBeforeAction.new(
204
- node: node,
205
- parent: parent,
206
- name: first_argument,
207
- only: keywords['only'],
208
- except: keywords['except']
209
- )
210
- elsif first_argument.try(:type) == :lambda_node
211
- ParsedBeforeAction.new(
212
- node: node,
213
- parent: parent,
214
- only: keywords['only'],
215
- except: keywords['except']
216
- )
217
- else
218
- fail(
219
- ArgumentError,
220
- "Cannot handle before_action with this argument #{first_argument.class}"
221
- )
222
- end
223
- @current_before_action = parent&.add_child(before_action)
244
+ if array_arguments.empty? && node.block.present?
245
+ # No need to add data
246
+ elsif first_argument.is_a?(String)
247
+ before_action.name = first_argument
248
+ before_action.only = keywords["only"]
249
+ before_action.except = keywords["except"]
250
+ elsif first_argument.try(:type) == :lambda_node
251
+ before_action.only = keywords["only"]
252
+ before_action.except = keywords["except"]
253
+ else
254
+ fail(
255
+ ArgumentError,
256
+ "Cannot handle before_action with this argument #{first_argument.class}"
257
+ )
258
+ end
224
259
 
225
260
  yield
226
261
  ensure
@@ -229,36 +264,65 @@ module I18n::Tasks::Scanners::PrismScanners
229
264
 
230
265
  def rails_handle_model_name(node)
231
266
  _args, kwargs = process_arguments(node)
232
- model_name = node.receiver.receiver.name.to_s.underscore
233
267
 
234
- count_key = (kwargs['count'] || 0) > 1 ? 'other' : 'one'
268
+ # We need to check for `node.receiver` since node is the `human` call
269
+ model_name = if current_class.present? && rails_model_method_called_on_current_class?(node.receiver)
270
+ current_class.path.flatten.map!(&:underscore).join(".")
271
+ elsif node.receiver.present?
272
+ node.receiver&.receiver&.name&.to_s&.underscore
273
+ end
274
+
275
+ return if model_name.nil?
276
+
277
+ # Handle count being a symbol, e.g. count: :other
278
+ count_key = case kwargs["count"]
279
+ when Integer
280
+ (kwargs["count"] > 1) ? "other" : "one"
281
+ when "one", :one, nil
282
+ "one"
283
+ else
284
+ "other"
285
+ end
286
+
235
287
  parent.add_translation_call(
236
288
  TranslationCall.new(
237
289
  node: node,
238
290
  receiver: nil,
239
- key: [:activerecord, :models, model_name, count_key].join('.'),
291
+ key: [:activerecord, :models, model_name, count_key].join("."),
240
292
  parent: parent,
241
293
  options: kwargs
242
294
  )
243
295
  )
244
296
  end
245
297
 
298
+ def rails_model_method_called_on_current_class?(node)
299
+ node.receiver.nil? || (node.receiver&.name&.to_s == "class" && node.receiver.receiver&.is_a?(Prism::SelfNode))
300
+ end
301
+
246
302
  def rails_handle_human_attribute_name(node)
247
303
  array_args, keywords = process_arguments(node)
248
- unless array_args.size == 1 && keywords.empty?
249
- fail(
250
- ArgumentError,
251
- 'human_attribute_name should have only one argument'
252
- )
304
+ # Arguments empty or cannot be processed, e.g. if it is a call
305
+ return unless array_args.size == 1 && keywords.empty?
306
+
307
+ # Handle if called on `self.class` or if the current_class has `model_name.i18n_key`
308
+ key = if current_class.present? && rails_model_method_called_on_current_class?(node)
309
+ [
310
+ :activerecord,
311
+ :attributes,
312
+ current_class.path.flatten.map!(&:underscore).join("."),
313
+ array_args.first
314
+ ].join(".")
315
+ elsif node.receiver&.name.present?
316
+ [
317
+ :activerecord,
318
+ :attributes,
319
+ node.receiver.name.to_s.underscore,
320
+ array_args.first
321
+ ].join(".")
322
+ else
323
+ return
253
324
  end
254
325
 
255
- key = [
256
- :activerecord,
257
- :attributes,
258
- node.receiver.name.to_s.underscore,
259
- array_args.first
260
- ].join('.')
261
-
262
326
  parent.add_translation_call(
263
327
  TranslationCall.new(
264
328
  node: node,
@@ -10,16 +10,16 @@ module I18n
10
10
  # @param calling_method [#call, Symbol, String, false, nil]
11
11
  # @return [String] absolute version of the key
12
12
  def absolute_key(key, path, roots: config[:relative_roots],
13
- exclude_method_name_paths: config[:relative_exclude_method_name_paths],
14
- calling_method: nil)
13
+ exclude_method_name_paths: config[:relative_exclude_method_name_paths],
14
+ calling_method: nil)
15
15
  return key unless key.start_with?(DOT)
16
- fail 'roots argument is required' unless roots.present?
16
+ fail "roots argument is required" unless roots.present?
17
17
 
18
18
  normalized_path = File.expand_path(path)
19
19
  (root = path_root(normalized_path, roots)) ||
20
20
  fail(CommandError, "Cannot resolve relative key \"#{key}\".\n" \
21
21
  "Set search.relative_roots in config/i18n-tasks.yml (currently #{roots.inspect})")
22
- normalized_path.sub!(root, '')
22
+ normalized_path.sub!(root, "")
23
23
 
24
24
  if (exclude_method_name_paths || []).map { |p| expand_path(p) }.include?(root)
25
25
  "#{prefix(normalized_path)}#{key}"
@@ -30,7 +30,7 @@ module I18n
30
30
 
31
31
  private
32
32
 
33
- DOT = '.'
33
+ DOT = "."
34
34
 
35
35
  # Detect the appropriate relative path root
36
36
  # @param [String] path /full/path
@@ -54,14 +54,14 @@ module I18n
54
54
  # @param normalized_path [String] path/relative/to/a/root
55
55
  # @param calling_method [#call, Symbol, String, false, nil]
56
56
  def prefix(normalized_path, calling_method: nil)
57
- file_key = normalized_path.gsub(%r{(\.[^/]+)*$}, '').tr(File::SEPARATOR, DOT)
57
+ file_key = normalized_path.gsub(%r{(\.[^/]+)*$}, "").tr(File::SEPARATOR, DOT)
58
58
  calling_method = calling_method.call if calling_method.respond_to?(:call)
59
59
  if calling_method&.present?
60
60
  # Relative keys in mailers have a `_mailer` infix, but relative keys in controllers do not have one:
61
- "#{file_key.sub(/_controller$/, '')}.#{calling_method}"
61
+ "#{file_key.sub(/_controller$/, "")}.#{calling_method}"
62
62
  else
63
63
  # Remove _ prefix from partials
64
- file_key.gsub('._', DOT)
64
+ file_key.gsub("._", DOT)
65
65
  end
66
66
  end
67
67
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/results/occurrence'
3
+ require "i18n/tasks/scanners/results/occurrence"
4
4
 
5
5
  module I18n::Tasks::Scanners::Results
6
6
  # A scanned key and all its occurrences.
@@ -14,7 +14,7 @@ module I18n::Tasks::Scanners::Results
14
14
  attr_reader :occurrences
15
15
 
16
16
  def initialize(key:, occurrences:)
17
- @key = key
17
+ @key = key
18
18
  @occurrences = occurrences
19
19
  end
20
20
 
@@ -31,7 +31,7 @@ module I18n::Tasks::Scanners::Results
31
31
  end
32
32
 
33
33
  def inspect
34
- "KeyOccurrences(#{key.inspect}, [#{occurrences.map(&:inspect).join(', ')}])"
34
+ "KeyOccurrences(#{key.inspect}, [#{occurrences.map(&:inspect).join(", ")}])"
35
35
  end
36
36
 
37
37
  # Merge {KeyOccurrences} in an {Enumerable<KeyOccurrences>} so that in the resulting {Array<KeyOccurrences>}:
@@ -28,6 +28,9 @@ module I18n::Tasks
28
28
  # @return [String, nil] the raw key (for relative keys and references)
29
29
  attr_accessor :raw_key
30
30
 
31
+ # @return [Array<String>, nil] candidate keys that may be used at runtime
32
+ attr_reader :candidate_keys
33
+
31
34
  # @param path [String]
32
35
  # @param pos [Integer]
33
36
  # @param line_num [Integer]
@@ -36,24 +39,25 @@ module I18n::Tasks
36
39
  # @param raw_key [String, nil]
37
40
  # @param default_arg [String, nil]
38
41
  # rubocop:disable Metrics/ParameterLists
39
- def initialize(path:, pos:, line_num:, line_pos:, line:, raw_key: nil, default_arg: nil)
40
- @path = path
41
- @pos = pos
42
- @line_num = line_num
43
- @line_pos = line_pos
44
- @line = line
45
- @raw_key = raw_key
42
+ def initialize(path:, pos:, line_num:, line_pos:, line:, raw_key: nil, default_arg: nil, candidate_keys: nil)
43
+ @path = path
44
+ @pos = pos
45
+ @line_num = line_num
46
+ @line_pos = line_pos
47
+ @line = line
48
+ @raw_key = raw_key
46
49
  @default_arg = default_arg
50
+ @candidate_keys = candidate_keys
47
51
  end
48
52
  # rubocop:enable Metrics/ParameterLists
49
53
 
50
54
  def inspect
51
- "Occurrence(#{@path}:#{@line_num}, line_pos: #{@line_pos}, pos: #{@pos}, raw_key: #{@raw_key}, default_arg: #{@default_arg}, line: #{@line})" # rubocop:disable Layout/LineLength
55
+ "Occurrence(#{@path}:#{@line_num}, line_pos: #{@line_pos}, pos: #{@pos}, raw_key: #{@raw_key}, candidate_keys: #{@candidate_keys}, default_arg: #{@default_arg}, line: #{@line})" # rubocop:disable Layout/LineLength
52
56
  end
53
57
 
54
58
  def ==(other)
55
59
  other.path == @path && other.pos == @pos && other.line_num == @line_num && other.line == @line &&
56
- other.raw_key == @raw_key && other.default_arg == @default_arg
60
+ other.raw_key == @raw_key && other.default_arg == @default_arg && other.candidate_keys == @candidate_keys
57
61
  end
58
62
 
59
63
  def eql?(other)
@@ -61,7 +65,7 @@ module I18n::Tasks
61
65
  end
62
66
 
63
67
  def hash
64
- [@path, @pos, @line_num, @line_pos, @line, @default_arg].hash
68
+ [@path, @pos, @line_num, @line_pos, @line, @default_arg, @candidate_keys].hash
65
69
  end
66
70
 
67
71
  # @param raw_key [String]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ast'
3
+ require "ast"
4
4
 
5
5
  module I18n::Tasks::Scanners
6
6
  class RubyAstCallFinder
@@ -2,7 +2,7 @@
2
2
 
3
3
  module I18n::Tasks::Scanners
4
4
  module RubyKeyLiterals
5
- LITERAL_RE = /:?".+?"|:?'.+?'|:\w+/.freeze
5
+ LITERAL_RE = /:?".+?"|:?'.+?'|:\w+/
6
6
 
7
7
  # Match literals:
8
8
  # * String: '', "#{}"
@@ -15,16 +15,16 @@ module I18n::Tasks::Scanners
15
15
  # @param literal [String] e.g: "key", 'key', or :key.
16
16
  # @return [String] key
17
17
  def strip_literal(literal)
18
- literal = literal[1..] if literal[0] == ':'
19
- literal = literal[1..-2] if literal[0] == "'" || literal[0] == '"'
18
+ literal = literal[1..] if literal[0] == ":"
19
+ literal = literal[1..-2] if ["'", '"'].include?(literal[0])
20
20
  literal
21
21
  end
22
22
 
23
- VALID_KEY_CHARS = %r{(?:[[:word:]]|[-.?!:;À-ž\\/]|(?<=[\p{L}\d])\s(?=[\p{L}\d]))}.freeze
24
- VALID_KEY_RE = /^#{VALID_KEY_CHARS}+$/.freeze
23
+ VALID_KEY_CHARS = %r{(?:[[:word:]]|[-.?!:;À-ž\\/]|(?<=[\p{L}\d])\s(?=[\p{L}\d]))}
24
+ VALID_KEY_RE = /^#{VALID_KEY_CHARS}+$/
25
25
 
26
26
  def valid_key?(key)
27
- key =~ VALID_KEY_RE && !key.end_with?('.')
27
+ key =~ VALID_KEY_RE && !key.end_with?(".")
28
28
  end
29
29
  end
30
30
  end
@@ -18,7 +18,7 @@ module I18n::Tasks::Scanners
18
18
  def self.create_parser
19
19
  prev = $VERBOSE
20
20
  $VERBOSE = nil
21
- require 'parser/current'
21
+ require "parser/current"
22
22
  ::Parser::CurrentRuby.new
23
23
  ensure
24
24
  $VERBOSE = prev