i18n-tasks 1.0.15 → 1.1.1

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 +70 -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 +4 -3
  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 +123 -62
  74. data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +186 -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 +226 -0
  82. data/lib/i18n/tasks/scanners/scanner.rb +2 -2
  83. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +3 -24
  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,8 +94,9 @@ 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)
98
+ # Do not process other receivers than I18n, e.g. Service.translate(:key)
99
+ return if node.receiver.present? && !i18n_receiver?(node.receiver)
120
100
  parent.add_translation_call(
121
101
  TranslationCall.new(
122
102
  node: node,
@@ -127,24 +107,18 @@ module I18n::Tasks::Scanners::PrismScanners
127
107
  )
128
108
  )
129
109
  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
110
+ handled_call = handle_rails_call_node(node) { super } if @rails
111
+
112
+ return if handled_call
113
+
114
+ parent.add_call(node)
115
+
137
116
  end
138
117
 
139
118
  super
140
119
  end
141
120
 
142
121
  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
122
  @root.process
149
123
  end
150
124
 
@@ -163,27 +137,105 @@ module I18n::Tasks::Scanners::PrismScanners
163
137
  end
164
138
 
165
139
  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)
140
+ return if node.nil?
141
+ return if node.comments.empty?
142
+
143
+ node.comments.each do |comment|
144
+ content =
145
+ comment.respond_to?(:slice) ? comment.slice : comment.location.slice
146
+ match = content.match(MAGIC_COMMENT_PREFIX)
147
+
148
+ next if match.nil?
149
+
150
+ string =
151
+ content.gsub(MAGIC_COMMENT_PREFIX, "").delete("#").strip
152
+ visitor = Visitor.new
153
+ Prism
154
+ .parse(string)
155
+ .value
156
+ .accept(visitor)
157
+
158
+ # Process and remap the found translation calls to be for the found comment
159
+ visitor.process.each do |comment_node|
160
+ parent.add_translation_call(comment_node.with_node(node))
161
+ end
162
+ end
163
+ end
164
+
165
+ def i18n_receiver?(receiver)
166
+ case receiver.type
167
+ when :constant_read_node
168
+ receiver.name == :I18n
169
+ when :constant_path_node
170
+ receiver.parent.nil? && receiver.child&.name == :I18n
171
+ else
172
+ false
170
173
  end
171
174
  end
172
175
 
173
176
  # ---- Rails specific methods ----
174
177
  # Returns true if the node was handled
175
- def rails_call_node(node, &block)
178
+ def handle_rails_call_node(node, &)
179
+ # First, handle calls that should be matched everywhere
176
180
  case node.name
177
- when :before_action
178
- rails_handle_before_action(node, &block)
179
- true
180
181
  when :human_attribute_name
181
182
  rails_handle_human_attribute_name(node)
182
- true
183
+ return true
183
184
  when :human
184
- return false if node.receiver.name != :model_name
185
-
185
+ return false if node.receiver.nil? || node.receiver.name != :model_name
186
186
  rails_handle_model_name(node)
187
+ return true
188
+ end
189
+
190
+ if @current_class&.controller?
191
+ rails_handle_controller_call_node(node, &)
192
+ elsif @current_class&.mailer?
193
+ rails_handle_mailer_call_node(node, &)
194
+ else
195
+ false
196
+ end
197
+ end
198
+
199
+ def rails_handle_controller_call_node(node, &)
200
+ case node.name
201
+ when :before_action
202
+ rails_handle_before_action(node, &)
203
+ true
204
+ else
205
+ false
206
+ end
207
+ end
208
+
209
+ def rails_handle_mailer_call_node(node, &)
210
+ case node.name
211
+ when :mail
212
+ _array_args, keywords = process_arguments(node)
213
+ if keywords.key?("subject")
214
+ # Let traversal continue so nested calls (e.g., t('.subject') or default_i18n_subject)
215
+ # get processed by their own handlers.
216
+ false
217
+ else
218
+ parent.add_translation_call(
219
+ TranslationCall.new(
220
+ node: node,
221
+ receiver: nil,
222
+ key: ".subject",
223
+ parent: parent,
224
+ options: {}
225
+ )
226
+ )
227
+ true
228
+ end
229
+ when :default_i18n_subject
230
+ parent.add_translation_call(
231
+ TranslationCall.new(
232
+ node: node,
233
+ receiver: nil,
234
+ key: ".subject",
235
+ parent: parent,
236
+ options: {}
237
+ )
238
+ )
187
239
  true
188
240
  else
189
241
  false
@@ -191,36 +243,32 @@ module I18n::Tasks::Scanners::PrismScanners
191
243
  end
192
244
 
193
245
  def rails_handle_before_action(node) # rubocop:disable Metrics/MethodLength
246
+ before_action = ParsedBeforeAction.new(
247
+ node: node,
248
+ parent: @current_class
249
+ )
250
+ @current_class&.add_child(before_action)
251
+ # Need to set current_before_action before processing arguments
252
+ # since they can be lambdas that need to be processed in the context of the before_action
253
+ @current_before_action = before_action
194
254
  array_arguments, keywords = process_arguments(node)
195
255
  first_argument = array_arguments.first
196
256
 
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)
257
+ if array_arguments.empty? && node.block.present?
258
+ # No need to add data
259
+ elsif first_argument.is_a?(String)
260
+ before_action.name = first_argument
261
+ before_action.only = keywords["only"]
262
+ before_action.except = keywords["except"]
263
+ elsif first_argument.try(:type) == :lambda_node
264
+ before_action.only = keywords["only"]
265
+ before_action.except = keywords["except"]
266
+ else
267
+ fail(
268
+ ArgumentError,
269
+ "Cannot handle before_action with this argument #{first_argument.class}"
270
+ )
271
+ end
224
272
 
225
273
  yield
226
274
  ensure
@@ -229,40 +277,73 @@ module I18n::Tasks::Scanners::PrismScanners
229
277
 
230
278
  def rails_handle_model_name(node)
231
279
  _args, kwargs = process_arguments(node)
232
- model_name = node.receiver.receiver.name.to_s.underscore
233
280
 
234
- count_key = (kwargs['count'] || 0) > 1 ? 'other' : 'one'
281
+ # We need to check for `node.receiver` since node is the `human` call
282
+ model_name = if current_class.present? && rails_model_method_called_on_current_class?(node.receiver)
283
+ current_class.path.flatten.map!(&:underscore).join(".")
284
+ elsif node.receiver&.receiver&.type == :constant_read_node
285
+ node.receiver&.receiver&.name&.to_s&.underscore
286
+ end
287
+
288
+ return if model_name.nil?
289
+
290
+ # Handle count being a symbol, e.g. count: :other
291
+ count_key = case kwargs["count"]
292
+ when Integer
293
+ (kwargs["count"] > 1) ? "other" : "one"
294
+ when "one", :one, nil
295
+ "one"
296
+ else
297
+ "other"
298
+ end
299
+
235
300
  parent.add_translation_call(
236
301
  TranslationCall.new(
237
302
  node: node,
238
303
  receiver: nil,
239
- key: [:activerecord, :models, model_name, count_key].join('.'),
304
+ key: [:activerecord, :models, model_name, count_key].join("."),
305
+ candidate_keys: [
306
+ [:activerecord, :models, model_name].join(".")
307
+ ],
240
308
  parent: parent,
241
309
  options: kwargs
242
310
  )
243
311
  )
244
312
  end
245
313
 
314
+ def rails_model_method_called_on_current_class?(node)
315
+ node.receiver.nil? || (node.receiver&.name&.to_s == "class" && node.receiver.receiver&.is_a?(Prism::SelfNode))
316
+ end
317
+
246
318
  def rails_handle_human_attribute_name(node)
247
319
  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
- )
320
+ # Arguments empty or cannot be processed, e.g. if it is a call
321
+ return unless array_args.size == 1 && keywords.empty?
322
+
323
+ # Handle if called on `self.class` or if the current_class has `model_name.i18n_key`
324
+ key = if current_class.present? && rails_model_method_called_on_current_class?(node)
325
+ [
326
+ :activerecord,
327
+ :attributes,
328
+ current_class.path.flatten.map!(&:underscore).join("."),
329
+ array_args.first
330
+ ].join(".")
331
+ elsif node.receiver&.name.present?
332
+ [
333
+ :activerecord,
334
+ :attributes,
335
+ node.receiver.name.to_s.underscore,
336
+ array_args.first
337
+ ].join(".")
338
+ else
339
+ return
253
340
  end
254
341
 
255
- key = [
256
- :activerecord,
257
- :attributes,
258
- node.receiver.name.to_s.underscore,
259
- array_args.first
260
- ].join('.')
261
-
262
342
  parent.add_translation_call(
263
343
  TranslationCall.new(
264
344
  node: node,
265
345
  key: key,
346
+ candidate_keys: Array([:attributes, array_args.first].join(".")),
266
347
  receiver: nil,
267
348
  parent: parent,
268
349
  options: {}
@@ -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