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,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/file_scanner'
4
- require 'i18n/tasks/scanners/relative_keys'
5
- require 'i18n/tasks/scanners/occurrence_from_position'
6
- require 'i18n/tasks/scanners/ruby_key_literals'
3
+ require "i18n/tasks/scanners/file_scanner"
4
+ require "i18n/tasks/scanners/relative_keys"
5
+ require "i18n/tasks/scanners/occurrence_from_position"
6
+ require "i18n/tasks/scanners/ruby_key_literals"
7
7
 
8
8
  module I18n::Tasks::Scanners
9
9
  # Maps the provided patterns to keys.
@@ -34,14 +34,14 @@ module I18n::Tasks::Scanners
34
34
  @patterns.flat_map do |pattern, key|
35
35
  result = []
36
36
  text.scan(pattern) do |_|
37
- match = Regexp.last_match
38
- matches = match.names.map(&:to_sym).zip(match.captures).to_h
37
+ match = Regexp.last_match
38
+ matches = match.names.map(&:to_sym).zip(match.captures).to_h
39
39
  if matches.key?(:key)
40
40
  matches[:key] = strip_literal(matches[:key])
41
41
  next unless valid_key?(matches[:key])
42
42
  end
43
43
  result << [absolute_key(format(key, matches), path),
44
- occurrence_from_position(path, text, match.offset(0).first)]
44
+ occurrence_from_position(path, text, match.offset(0).first)]
45
45
  end
46
46
  result
47
47
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/file_scanner'
4
- require 'i18n/tasks/scanners/relative_keys'
5
- require 'i18n/tasks/scanners/occurrence_from_position'
6
- require 'i18n/tasks/scanners/ruby_key_literals'
3
+ require "i18n/tasks/scanners/file_scanner"
4
+ require "i18n/tasks/scanners/relative_keys"
5
+ require "i18n/tasks/scanners/occurrence_from_position"
6
+ require "i18n/tasks/scanners/ruby_key_literals"
7
7
 
8
8
  module I18n::Tasks::Scanners
9
9
  # Scan for I18n.t usages using a simple regular expression.
@@ -12,15 +12,15 @@ module I18n::Tasks::Scanners
12
12
  include OccurrenceFromPosition
13
13
  include RubyKeyLiterals
14
14
 
15
- TRANSLATE_CALL_RE = /(?<=^|[^\p{L}_'\-.]|[^\p{L}'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
15
+ TRANSLATE_CALL_RE = /(?<=^|[^\p{L}_'\-.]|[^\p{L}'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/
16
16
  IGNORE_LINES = {
17
- 'coffee' => /^\s*#(?!\si18n-tasks-use)/,
18
- 'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
19
- 'es6' => %r{^\s*//(?!\si18n-tasks-use)},
20
- 'haml' => /^\s*-\s*#(?!\si18n-tasks-use)/,
21
- 'js' => %r{^\s*//(?!\si18n-tasks-use)},
22
- 'opal' => /^\s*#(?!\si18n-tasks-use)/,
23
- 'slim' => %r{^\s*(?:-#|/)(?!\si18n-tasks-use)}
17
+ "coffee" => /^\s*#(?!\si18n-tasks-use)/,
18
+ "erb" => /^\s*<%\s*#(?!\si18n-tasks-use)/,
19
+ "es6" => %r{^\s*//(?!\si18n-tasks-use)},
20
+ "haml" => /^\s*-\s*#(?!\si18n-tasks-use)/,
21
+ "js" => %r{^\s*//(?!\si18n-tasks-use)},
22
+ "opal" => /^\s*#(?!\si18n-tasks-use)/,
23
+ "slim" => %r{^\s*(?:-#|/)(?!\si18n-tasks-use)}
24
24
  }.freeze
25
25
 
26
26
  def initialize(**args)
@@ -47,13 +47,13 @@ module I18n::Tasks::Scanners
47
47
  key = match_to_key(match, path, location)
48
48
  next unless key
49
49
 
50
- key += ':' if key.end_with?('.')
50
+ key += ":" if key.end_with?(".")
51
51
  next unless valid_key?(key)
52
52
 
53
53
  keys << [key, location]
54
54
  end
55
55
  keys
56
- rescue Exception => e # rubocop:disable Lint/RescueException
56
+ rescue => e
57
57
  raise ::I18n::Tasks::CommandError.new(e, "Error scanning #{path}: #{e.message}")
58
58
  end
59
59
 
@@ -62,7 +62,7 @@ module I18n::Tasks::Scanners
62
62
  # @return [String] full absolute key name
63
63
  def match_to_key(match, path, location)
64
64
  absolute_key(strip_literal(match[0]), path,
65
- calling_method: -> { closest_method(location) if key_relative_to_method?(path) })
65
+ calling_method: -> { closest_method(location) if key_relative_to_method?(path) })
66
66
  end
67
67
 
68
68
  def exclude_line?(line, path)
@@ -70,11 +70,11 @@ module I18n::Tasks::Scanners
70
70
  re && re =~ line
71
71
  end
72
72
 
73
- VALID_KEY_RE_DYNAMIC = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/.freeze
73
+ VALID_KEY_RE_DYNAMIC = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
74
74
 
75
75
  def valid_key?(key)
76
76
  if @config[:strict]
77
- super(key)
77
+ super
78
78
  else
79
79
  key =~ VALID_KEY_RE_DYNAMIC
80
80
  end
@@ -85,9 +85,9 @@ module I18n::Tasks::Scanners
85
85
  end
86
86
 
87
87
  def closest_method(occurrence)
88
- method = File.readlines(occurrence.path, encoding: 'UTF-8')
89
- .first(occurrence.line_num - 1).reverse_each.find { |x| x =~ /\bdef\b/ }
90
- method && method.strip.sub(/^def\s*/, '').sub(/[(\s;].*$/, '')
88
+ method = File.readlines(occurrence.path, encoding: "UTF-8")
89
+ .first(occurrence.line_num - 1).reverse_each.find { |x| x =~ /\bdef\b/ }
90
+ method && method.strip.sub(/^def\s*/, "").sub(/[(\s;].*$/, "")
91
91
  end
92
92
 
93
93
  # This method only exists for backwards compatibility with monkey-patches and plugins
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/pattern_scanner'
3
+ require "i18n/tasks/scanners/pattern_scanner"
4
4
 
5
5
  module I18n::Tasks::Scanners
6
6
  # Scans for I18n.t(key, scope: ...) usages
@@ -21,13 +21,13 @@ module I18n::Tasks::Scanners
21
21
  # @param [String] path
22
22
  # @return [String] full absolute key name with scope resolved if any
23
23
  def match_to_key(match, path, location)
24
- key = super
24
+ key = super
25
25
  scope = match[1]
26
26
  if scope
27
27
  scope_parts = extract_literal_or_array_of_literals(scope)
28
28
  return nil if scope_parts.nil? || scope_parts.empty?
29
29
 
30
- "#{scope_parts.join('.')}.#{key}"
30
+ "#{scope_parts.join(".")}.#{key}"
31
31
  else
32
32
  key unless match[0] =~ /\A\w/
33
33
  end
@@ -43,7 +43,7 @@ module I18n::Tasks::Scanners
43
43
  if val =~ /\A[\w@]/
44
44
  "\#{#{val}}"
45
45
  else
46
- super(val)
46
+ super
47
47
  end
48
48
  end
49
49
 
@@ -77,18 +77,18 @@ module I18n::Tasks::Scanners
77
77
  end
78
78
  end
79
79
  s.each_char.with_index do |c, i|
80
- if c == '['
80
+ if c == "["
81
81
  return nil unless braces_stack.empty?
82
82
 
83
83
  braces_stack.push(i)
84
- elsif c == ']'
84
+ elsif c == "]"
85
85
  break
86
- elsif c == ','
86
+ elsif c == ","
87
87
  consume_literal.call
88
88
  break if braces_stack.empty?
89
89
  elsif c =~ VALID_KEY_CHARS || /['":]/ =~ c
90
90
  acc << c
91
- elsif c != ' '
91
+ elsif c != " "
92
92
  return nil
93
93
  end
94
94
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'prism/visitor'
3
+ require "prism/visitor"
4
4
 
5
5
  # This class is used to parse the arguments to e.g. a Prism::CallNode and return the values we need
6
6
  # for turning them into translations and occurrences.
@@ -9,11 +9,18 @@ module I18n::Tasks::Scanners::PrismScanners
9
9
  class ArgumentsVisitor < Prism::Visitor
10
10
  def visit_keyword_hash_node(node)
11
11
  node.child_nodes.each_with_object({}) do |child, hash|
12
+ next if child.type == :assoc_splat_node
13
+
12
14
  hash[visit(child.key)] = visit(child.value)
13
15
  hash
14
16
  end
15
17
  end
16
18
 
19
+ # Cannot handle arguments that are calls
20
+ def visit_call_node(_node)
21
+ nil
22
+ end
23
+
17
24
  def visit_symbol_node(node)
18
25
  node.value
19
26
  end
@@ -4,14 +4,16 @@
4
4
  # Used in the PrismScanners::Visitor class.
5
5
  module I18n::Tasks::Scanners::PrismScanners
6
6
  class Root
7
- attr_reader(:calls, :translation_calls, :children, :node, :parent)
7
+ attr_reader(:calls, :translation_calls, :children, :node, :parent, :rails, :file_path)
8
8
 
9
- def initialize(node: nil, parent: nil)
9
+ def initialize(node: nil, parent: nil, file_path: nil, rails: false)
10
10
  @calls = []
11
11
  @translation_calls = []
12
12
  @children = []
13
13
  @node = node
14
14
  @parent = parent
15
+ @rails = rails
16
+ @file_path = file_path
15
17
  end
16
18
 
17
19
  def add_child(node)
@@ -24,39 +26,61 @@ module I18n::Tasks::Scanners::PrismScanners
24
26
  end
25
27
 
26
28
  def add_translation_call(translation_call)
27
- @translation_calls << translation_call
29
+ @translation_calls += Array(translation_call)
30
+ end
31
+
32
+ def rails_view?
33
+ rails && file_path.present? && file_path.include?("app/views/")
34
+ end
35
+
36
+ def partial_view?
37
+ file_path.present? && File.basename(file_path).start_with?("_")
28
38
  end
29
39
 
30
40
  def support_relative_keys?
31
- false
41
+ rails_view? && !partial_view?
32
42
  end
33
43
 
34
- def private_method
44
+ def support_candidate_keys?
35
45
  false
36
46
  end
37
47
 
38
48
  def path
39
- []
49
+ if rails_view?
50
+ folder_path = file_path.sub(%r{app/views/}, "").split("/")
51
+ name = folder_path.pop.split(".").first
52
+
53
+ [*folder_path, name]
54
+ else
55
+ []
56
+ end
40
57
  end
41
58
 
42
59
  def process
43
60
  (@translation_calls + @children.flat_map(&:process)).flatten
44
61
  end
62
+
63
+ # Only supported for Rails controllers currently
64
+ def private_method
65
+ false
66
+ end
45
67
  end
46
68
 
47
69
  class TranslationCall
70
+ class ScopeError < StandardError; end
48
71
  attr_reader(:node, :key, :receiver, :options, :parent)
49
72
 
50
- def initialize(node:, key:, receiver:, options:, parent:)
73
+ def initialize(node:, key:, receiver:, options:, parent:, candidate_keys: nil)
51
74
  @node = node
52
75
  @key = key
53
76
  @receiver = receiver
54
77
  @options = options
55
78
  @parent = parent
79
+ @candidate_keys = candidate_keys || []
56
80
  end
57
81
 
58
82
  def relative_key?
59
- @key&.start_with?('.') && @receiver.nil?
83
+ @key&.start_with?(".") && @receiver.nil?
60
84
  end
61
85
 
62
86
  def with_parent(parent)
@@ -83,34 +107,54 @@ module I18n::Tasks::Scanners::PrismScanners
83
107
  occurrence(file_path)
84
108
  end
85
109
 
110
+ # Returns either a single key string or an array of candidate key strings for this call.
86
111
  def full_key
87
112
  return nil if key.nil?
88
113
  return nil unless key.is_a?(String)
89
114
  return nil if relative_key? && !support_relative_keys?
90
115
 
91
- parts = [scope]
92
-
93
- if relative_key?
94
- parts.concat(parent&.path || [])
95
- parts << key
116
+ base_parts = [scope].compact
117
+
118
+ if relative_key? && support_candidate_keys?
119
+ # For relative keys in controllers/methods, generate candidate keys by
120
+ # progressively stripping trailing path segments from the parent path.
121
+ # Example: parent.path = ["events", "create"], key = ".success"
122
+ # yields: ["events.create.success", "events.success"]
123
+ parent_path = parent&.path || []
124
+ rel_key = key[1..] # strip leading dot # rubocop:disable Performance/ArraySemiInfiniteRangeSlice
125
+
126
+ candidates = []
127
+ parent_path_length = parent_path.length
128
+ # Do not generate an unscoped bare key (keep_count = 0). Start from full parent path
129
+ parent_path_length.downto(1) do |keep_count|
130
+ parts = base_parts + parent_path.first(keep_count) + [rel_key]
131
+ candidates << parts.compact.join(".")
132
+ end
96
133
 
97
- # TODO: Fallback to controller without action name
98
- elsif key.start_with?('.')
99
- parts << key[1..]
134
+ candidates.map { |c| c.gsub("..", ".") }
135
+ elsif relative_key?
136
+ # For relative keys in views, just append to the full path
137
+ [base_parts + parent.path + [key[1..]]].flatten.compact.join(".").gsub("..", ".") # rubocop:disable Performance/ChainArrayAllocation
138
+ elsif key.start_with?(".")
139
+ [base_parts + [key[1..]]].flatten.compact.join(".").gsub("..", ".") # rubocop:disable Performance/ArraySemiInfiniteRangeSlice,Performance/ChainArrayAllocation
140
+ elsif @candidate_keys.present?
141
+ ([key] + @candidate_keys).map do |c|
142
+ [base_parts + [c]].flatten.compact.join(".").gsub("..", ".") # rubocop:disable Performance/ChainArrayAllocation
143
+ end
100
144
  else
101
- parts << key
145
+ [base_parts + [key]].flatten.compact.join(".").gsub("..", ".") # rubocop:disable Performance/ChainArrayAllocation
102
146
  end
103
-
104
- parts.compact.join('.').gsub('..', '.')
105
147
  end
106
148
 
107
149
  private
108
150
 
109
151
  def scope
110
152
  return nil if @options.nil?
111
- return nil unless @options['scope']
153
+ return nil unless @options["scope"]
154
+
155
+ fail(ScopeError, "Could not process scope") if @options.key?("scope") && (Array(@options["scope"]).empty? || !Array(@options["scope"]).all? { |s| s.is_a?(String) || s.is_a?(Symbol) })
112
156
 
113
- Array(@options['scope']).compact.map(&:to_s).join('.')
157
+ Array(@options["scope"]).join(".")
114
158
  end
115
159
 
116
160
  def occurrence(file_path)
@@ -118,38 +162,38 @@ module I18n::Tasks::Scanners::PrismScanners
118
162
 
119
163
  location = local_node.location
120
164
 
121
- final_key = full_key
122
- return nil if final_key.nil?
123
-
124
- [
125
- final_key,
126
- ::I18n::Tasks::Scanners::Results::Occurrence.new(
127
- path: file_path,
128
- line: local_node.respond_to?(:slice) ? local_node.slice : local_node.location.slice,
129
- pos: location.start_offset,
130
- line_pos: location.start_column,
131
- line_num: location.start_line,
132
- raw_key: key
133
- )
134
- ]
135
- end
136
-
137
- def occurrences_from_comments(file_path)
138
- Array(@comment_translations).flat_map do |child_node|
139
- child_node.with_context(
140
- path: @path,
141
- options: {
142
- **@context_options,
143
- comment_for_node: @node
144
- }
145
- ).occurrences(file_path)
165
+ final = full_key
166
+ return nil if final.nil?
167
+
168
+ occurrence = ::I18n::Tasks::Scanners::Results::Occurrence.new(
169
+ path: file_path,
170
+ line: local_node.respond_to?(:slice) ? local_node.slice : local_node.location.slice,
171
+ pos: location.start_offset,
172
+ line_pos: location.start_column,
173
+ line_num: location.start_line,
174
+ raw_key: key,
175
+ candidate_keys: Array(final)
176
+ )
177
+
178
+ # full_key may be a single String or an Array of candidate strings
179
+ if final.is_a?(Array)
180
+ [final.first, occurrence]
181
+ else
182
+ [final, occurrence]
146
183
  end
184
+ rescue ScopeError
185
+ nil
147
186
  end
148
187
 
149
188
  # Only public methods are added to the context path
150
189
  # Only some classes supports relative keys
151
190
  def support_relative_keys?
152
- parent.is_a?(ParsedMethod) && parent.support_relative_keys?
191
+ (parent.is_a?(ParsedMethod) || parent.is_a?(Root)) && parent.support_relative_keys?
192
+ end
193
+
194
+ # Not supported for Rails views
195
+ def support_candidate_keys?
196
+ support_relative_keys? && parent.support_candidate_keys?
153
197
  end
154
198
  end
155
199
 
@@ -158,10 +202,6 @@ module I18n::Tasks::Scanners::PrismScanners
158
202
  false
159
203
  end
160
204
 
161
- def private_method
162
- false
163
- end
164
-
165
205
  def path
166
206
  (@parent&.path || []) + [path_name]
167
207
  end
@@ -179,9 +219,8 @@ module I18n::Tasks::Scanners::PrismScanners
179
219
  @methods = []
180
220
  @private_methods = []
181
221
  @before_actions = []
182
- @rails = rails
183
222
 
184
- super(node: node, parent: parent)
223
+ super
185
224
  end
186
225
 
187
226
  def add_child(node)
@@ -200,8 +239,14 @@ module I18n::Tasks::Scanners::PrismScanners
200
239
  end
201
240
 
202
241
  def process # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
203
- return @children.flat_map(&:process) unless controller?
242
+ if controller?
243
+ process_controller
244
+ else
245
+ super
246
+ end
247
+ end
204
248
 
249
+ def process_controller
205
250
  methods_by_name = @methods.group_by(&:name)
206
251
  private_methods_by_name = @private_methods.group_by(&:name)
207
252
 
@@ -221,7 +266,9 @@ module I18n::Tasks::Scanners::PrismScanners
221
266
  next unless before_action.applies_to?(method.name)
222
267
 
223
268
  method.add_translation_call(
224
- translation_calls.map { |call| call.with_parent(method) }
269
+ translation_calls.map do |call|
270
+ call.with_parent(method)
271
+ end
225
272
  )
226
273
  end
227
274
  end
@@ -240,7 +287,7 @@ module I18n::Tasks::Scanners::PrismScanners
240
287
  nested_calls[method.name] << other_method.name
241
288
 
242
289
  if nested_calls[call.name]&.include?(method.name)
243
- fail(ArgumentError, "Cyclic call detected: #{call.name} -> #{method.name}")
290
+ next
244
291
  end
245
292
 
246
293
  other_method.translation_calls.each do |translation_call|
@@ -249,7 +296,7 @@ module I18n::Tasks::Scanners::PrismScanners
249
296
  end
250
297
  end
251
298
 
252
- @children.flat_map(&:process) + new_translation_calls
299
+ @translation_calls + @children.flat_map(&:process) + new_translation_calls
253
300
  end
254
301
 
255
302
  def private_methods!
@@ -257,7 +304,11 @@ module I18n::Tasks::Scanners::PrismScanners
257
304
  end
258
305
 
259
306
  def support_relative_keys?
260
- @rails && controller?
307
+ controller? || mailer?
308
+ end
309
+
310
+ def support_candidate_keys?
311
+ controller?
261
312
  end
262
313
 
263
314
  def path
@@ -265,12 +316,16 @@ module I18n::Tasks::Scanners::PrismScanners
265
316
  end
266
317
 
267
318
  def controller?
268
- @node.name.to_s.end_with?('Controller')
319
+ @rails && @node.name.to_s.end_with?("Controller")
320
+ end
321
+
322
+ def mailer?
323
+ @rails && @node.name.to_s.end_with?("Mailer")
269
324
  end
270
325
 
271
326
  def path_name
272
327
  path = @node.constant_path.full_name_parts.map { |s| s.to_s.underscore }
273
- path.last.gsub!(/_controller\z/, '') if controller?
328
+ path.last.delete_suffix!("_controller") if controller?
274
329
 
275
330
  path
276
331
  end
@@ -287,6 +342,8 @@ module I18n::Tasks::Scanners::PrismScanners
287
342
  !@private_method && @parent&.support_relative_keys?
288
343
  end
289
344
 
345
+ delegate(:support_candidate_keys?, to: :parent)
346
+
290
347
  def path
291
348
  (@parent&.path || []) + [@node.name]
292
349
  end
@@ -301,7 +358,7 @@ module I18n::Tasks::Scanners::PrismScanners
301
358
  end
302
359
 
303
360
  class ParsedBeforeAction < Root
304
- attr_reader(:name)
361
+ attr_accessor(:name, :only, :except)
305
362
 
306
363
  def initialize(node:, parent:, name: nil, only: nil, except: nil)
307
364
  @name = name
@@ -312,7 +369,7 @@ module I18n::Tasks::Scanners::PrismScanners
312
369
  end
313
370
 
314
371
  def support_relative_keys?
315
- true
372
+ false
316
373
  end
317
374
 
318
375
  def applies_to?(method_name)
@@ -330,5 +387,9 @@ module I18n::Tasks::Scanners::PrismScanners
330
387
  def path
331
388
  @parent&.path || []
332
389
  end
390
+
391
+ def process
392
+ @translation_calls.filter { |call| !call.relative_key? }
393
+ end
333
394
  end
334
395
  end