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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/reports/base'
4
- require 'terminal-table'
3
+ require "i18n/tasks/reports/base"
4
+ require "terminal-table"
5
5
  module I18n
6
6
  module Tasks
7
7
  module Reports
@@ -10,17 +10,17 @@ module I18n
10
10
  forest = collapse_missing_tree! forest
11
11
  if forest.present?
12
12
  print_title missing_title(forest)
13
- print_table headings: [Rainbow(I18n.t('i18n_tasks.common.locale')).cyan.bright,
14
- Rainbow(I18n.t('i18n_tasks.common.key')).cyan.bright,
15
- I18n.t('i18n_tasks.missing.details_title')] do |t|
13
+ print_table headings: [Rainbow(I18n.t("i18n_tasks.common.locale")).cyan.bright,
14
+ Rainbow(I18n.t("i18n_tasks.common.key")).cyan.bright,
15
+ I18n.t("i18n_tasks.missing.details_title")] do |t|
16
16
  t.rows = sort_by_attr!(forest_to_attr(forest)).map do |a|
17
- [{ value: Rainbow(format_locale(a[:locale])).cyan, alignment: :center },
18
- format_key(a[:key], a[:data]),
19
- missing_key_info(a)]
17
+ [{value: Rainbow(format_locale(a[:locale])).cyan, alignment: :center},
18
+ format_key(a[:key], a[:data]),
19
+ missing_key_info(a)]
20
20
  end
21
21
  end
22
22
  else
23
- print_success I18n.t('i18n_tasks.missing.none')
23
+ print_success I18n.t("i18n_tasks.missing.none")
24
24
  end
25
25
  end
26
26
 
@@ -29,7 +29,24 @@ module I18n
29
29
  print_title inconsistent_interpolations_title(forest)
30
30
  show_tree(forest)
31
31
  else
32
- print_success I18n.t('i18n_tasks.inconsistent_interpolations.none')
32
+ print_success I18n.t("i18n_tasks.inconsistent_interpolations.none")
33
+ end
34
+ end
35
+
36
+ def reserved_interpolations(forest = task.reserved_interpolations)
37
+ if forest.present?
38
+ print_title reserved_interpolations_title(forest)
39
+ print_table headings: [Rainbow(I18n.t("i18n_tasks.common.locale")).cyan.bright,
40
+ Rainbow(I18n.t("i18n_tasks.common.key")).cyan.bright,
41
+ I18n.t("i18n_tasks.reserved_interpolations.details_title")] do |t|
42
+ t.rows = sort_by_attr!(forest_to_attr(forest)).map do |a|
43
+ [{value: Rainbow(format_locale(a[:locale])).cyan, alignment: :center},
44
+ format_key(a[:key], a[:data]),
45
+ a[:value].join(", ")]
46
+ end
47
+ end
48
+ else
49
+ print_success I18n.t("i18n_tasks.reserved_interpolations.none")
33
50
  end
34
51
  end
35
52
 
@@ -45,7 +62,7 @@ module I18n
45
62
  print_occurrences node, key
46
63
  end
47
64
  else
48
- print_error I18n.t('i18n_tasks.usages.none')
65
+ print_error I18n.t("i18n_tasks.usages.none")
49
66
  end
50
67
  end
51
68
 
@@ -55,7 +72,7 @@ module I18n
55
72
  print_title unused_title(keys)
56
73
  print_locale_key_value_data_table keys
57
74
  else
58
- print_success I18n.t('i18n_tasks.unused.none')
75
+ print_success I18n.t("i18n_tasks.unused.none")
59
76
  end
60
77
  end
61
78
 
@@ -65,7 +82,7 @@ module I18n
65
82
  print_title eq_base_title(keys)
66
83
  print_locale_key_value_data_table keys
67
84
  else
68
- print_info Rainbow('No translations are the same as base value').cyan
85
+ print_info Rainbow("No translations are the same as base value").cyan
69
86
  end
70
87
  end
71
88
 
@@ -74,39 +91,39 @@ module I18n
74
91
  end
75
92
 
76
93
  def forest_stats(forest, stats = task.forest_stats(forest))
77
- text = if stats[:locale_count] == 1
78
- I18n.t('i18n_tasks.data_stats.text_single_locale', **stats)
79
- else
80
- I18n.t('i18n_tasks.data_stats.text', **stats)
81
- end
82
- title = Rainbow(I18n.t('i18n_tasks.data_stats.title', **stats.slice(:locales))).bright
94
+ text = if stats[:locale_count] == 1
95
+ I18n.t("i18n_tasks.data_stats.text_single_locale", **stats)
96
+ else
97
+ I18n.t("i18n_tasks.data_stats.text", **stats)
98
+ end
99
+ title = Rainbow(I18n.t("i18n_tasks.data_stats.title", **stats.slice(:locales))).bright
83
100
  print_info "#{Rainbow(title).cyan} #{Rainbow(text).cyan}"
84
101
  end
85
102
 
86
103
  def mv_results(results)
87
104
  results.each do |(from, to)|
88
105
  if to
89
- print_info "#{Rainbow(from).cyan} #{Rainbow('').yellow.bright} #{Rainbow(to).cyan}"
106
+ print_info "#{Rainbow(from).cyan} #{Rainbow("").yellow.bright} #{Rainbow(to).cyan}"
90
107
  else
91
- print_info "#{Rainbow(from).red}#{Rainbow(' 🗑').red.bright}"
108
+ print_info "#{Rainbow(from).red}#{Rainbow(" 🗑").red.bright}"
92
109
  end
93
110
  end
94
111
  end
95
112
 
96
113
  def cp_results(results)
97
114
  results.each do |(from, to)|
98
- print_info "#{Rainbow(from).cyan} #{Rainbow('+').yellow.bright} #{Rainbow(to).green}"
115
+ print_info "#{Rainbow(from).cyan} #{Rainbow("+").yellow.bright} #{Rainbow(to).green}"
99
116
  end
100
117
  end
101
118
 
102
119
  def check_normalized_results(non_normalized)
103
120
  if non_normalized.empty?
104
- print_success 'All data is normalized'
121
+ print_success "All data is normalized"
105
122
  return
106
123
  end
107
- log_stderr Rainbow('The following data requires normalization:').yellow
124
+ log_stderr Rainbow("The following data requires normalization:").yellow
108
125
  puts non_normalized
109
- log_stderr Rainbow('Run `i18n-tasks normalize` to fix').yellow
126
+ log_stderr Rainbow("Run `i18n-tasks normalize` to fix").yellow
110
127
  end
111
128
 
112
129
  private
@@ -116,7 +133,7 @@ module I18n
116
133
  when :missing_used
117
134
  first_occurrence leaf
118
135
  when :missing_plural
119
- leaf[:data][:missing_keys].join(', ')
136
+ leaf[:data][:missing_keys].join(", ")
120
137
  else
121
138
  "#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} " \
122
139
  "#{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
@@ -127,16 +144,16 @@ module I18n
127
144
  if data[:ref_info]
128
145
  from, to = data[:ref_info]
129
146
  resolved = key[0...to.length]
130
- after = key[to.length..]
147
+ after = key[to.length..]
131
148
  " #{Rainbow(from).yellow}#{Rainbow(after).cyan}\n" \
132
- "#{Rainbow('').yellow.bright} #{Rainbow(resolved).yellow.bright}"
149
+ "#{Rainbow("").yellow.bright} #{Rainbow(resolved).yellow.bright}"
133
150
  else
134
151
  Rainbow(key).cyan
135
152
  end
136
153
  end
137
154
 
138
155
  def format_value(val)
139
- val.is_a?(Symbol) ? "#{Rainbow('').yellow.bright}#{Rainbow(val).yellow}" : val.to_s.strip
156
+ val.is_a?(Symbol) ? "#{Rainbow("").yellow.bright}#{Rainbow(val).yellow}" : val.to_s.strip
140
157
  end
141
158
 
142
159
  def format_reference_desc(node_data)
@@ -144,19 +161,19 @@ module I18n
144
161
 
145
162
  case node_data[:ref_type]
146
163
  when :reference_usage
147
- Rainbow('(ref)').yellow.bright
164
+ Rainbow("(ref)").yellow.bright
148
165
  when :reference_usage_resolved
149
- Rainbow('(resolved ref)').yellow.bright
166
+ Rainbow("(resolved ref)").yellow.bright
150
167
  when :reference_usage_key
151
- Rainbow('(ref key)').yellow.bright
168
+ Rainbow("(ref key)").yellow.bright
152
169
  end
153
170
  end
154
171
 
155
172
  def print_occurrences(node, full_key = node.full_key)
156
173
  occurrences = node.data[:occurrences]
157
174
  puts [Rainbow(full_key).bright,
158
- format_reference_desc(node.data),
159
- (Rainbow(occurrences.size).green if occurrences.size > 1)].compact.join ' '
175
+ format_reference_desc(node.data),
176
+ (Rainbow(occurrences.size).green if occurrences.size > 1)].compact.join " "
160
177
  occurrences.each do |occurrence|
161
178
  puts " #{key_occurrence full_key, occurrence}"
162
179
  end
@@ -164,25 +181,25 @@ module I18n
164
181
 
165
182
  def print_locale_key_value_data_table(locale_key_value_datas)
166
183
  if locale_key_value_datas.present?
167
- print_table headings: [Rainbow(I18n.t('i18n_tasks.common.locale')).cyan.bright,
168
- Rainbow(I18n.t('i18n_tasks.common.key')).cyan.bright,
169
- I18n.t('i18n_tasks.common.value')] do |t|
184
+ print_table headings: [Rainbow(I18n.t("i18n_tasks.common.locale")).cyan.bright,
185
+ Rainbow(I18n.t("i18n_tasks.common.key")).cyan.bright,
186
+ I18n.t("i18n_tasks.common.value")] do |t|
170
187
  t.rows = locale_key_value_datas.map do |(locale, k, v, data)|
171
- [{ value: Rainbow(locale).cyan, alignment: :center }, format_key(k, data), format_value(v)]
188
+ [{value: Rainbow(locale).cyan, alignment: :center}, format_key(k, data), format_value(v)]
172
189
  end
173
190
  end
174
191
  else
175
- puts 'ø'
192
+ puts "ø"
176
193
  end
177
194
  end
178
195
 
179
196
  def print_title(title)
180
- log_stderr "#{Rainbow(title.strip).bright} #{Rainbow('|').faint} " \
197
+ log_stderr "#{Rainbow(title.strip).bright} #{Rainbow("|").faint} " \
181
198
  "#{"i18n-tasks v#{I18n::Tasks::VERSION}"}"
182
199
  end
183
200
 
184
201
  def print_success(message)
185
- log_stderr Rainbow("✓ #{I18n.t('i18n_tasks.cmd.encourage').sample} #{message}").green.bright
202
+ log_stderr Rainbow("✓ #{I18n.t("i18n_tasks.cmd.encourage").sample} #{message}").green.bright
186
203
  end
187
204
 
188
205
  def print_error(message)
@@ -194,16 +211,16 @@ module I18n
194
211
  end
195
212
 
196
213
  def indent(txt, n = 2)
197
- txt.gsub(/^/, ' ' * n)
214
+ txt.gsub(/^/, " " * n)
198
215
  end
199
216
 
200
- def print_table(opts, &block)
201
- puts ::Terminal::Table.new(opts, &block)
217
+ def print_table(opts, &)
218
+ puts ::Terminal::Table.new(opts, &)
202
219
  end
203
220
 
204
221
  def key_occurrence(full_key, occurrence)
205
222
  location = Rainbow("#{occurrence.path}:#{occurrence.line_num}").green
206
- source = highlight_key(occurrence.raw_key || full_key, occurrence.line, occurrence.line_pos..-1).strip
223
+ source = highlight_key(occurrence.raw_key || full_key, occurrence.line, occurrence.line_pos..-1).strip
207
224
  "#{location} #{source}"
208
225
  end
209
226
 
@@ -214,8 +231,8 @@ module I18n
214
231
  first = occurrences.first
215
232
  [
216
233
  Rainbow("#{first.path}:#{first.line_num}").green,
217
- ("(#{I18n.t 'i18n_tasks.common.n_more', count: occurrences.length - 1})" if occurrences.length > 1)
218
- ].compact.join(' ')
234
+ ("(#{I18n.t "i18n_tasks.common.n_more", count: occurrences.length - 1})" if occurrences.length > 1)
235
+ ].compact.join(" ")
219
236
  end
220
237
 
221
238
  def highlight_key(full_key, line, range = (0..-1))
@@ -7,7 +7,7 @@ module I18n::Tasks::Scanners::AstMatchers
7
7
  end
8
8
 
9
9
  def convert_to_key_occurrences(send_node, _method_name, location: send_node.loc)
10
- fail('Not implemented')
10
+ fail("Not implemented")
11
11
  end
12
12
 
13
13
  protected
@@ -32,7 +32,7 @@ module I18n::Tasks::Scanners::AstMatchers
32
32
  elsif %i[true false].include?(node.type)
33
33
  node.type.to_s
34
34
  elsif node.type == :nil
35
- ''
35
+ ""
36
36
  elsif node.type == :array && array_join_with
37
37
  extract_array_as_string(
38
38
  node,
@@ -109,7 +109,7 @@ module I18n::Tasks::Scanners::AstMatchers
109
109
  if array_reject_blank
110
110
  children_strings.reject! do |x|
111
111
  # empty strings and nils in the scope argument are ignored by i18n
112
- x == ''
112
+ x == ""
113
113
  end
114
114
  end
115
115
  children_strings.join(array_join_with)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/ast_matchers/base_matcher'
4
- require 'i18n/tasks/scanners/results/occurrence'
3
+ require "i18n/tasks/scanners/ast_matchers/base_matcher"
4
+ require "i18n/tasks/scanners/results/occurrence"
5
5
 
6
6
  module I18n::Tasks::Scanners::AstMatchers
7
7
  class DefaultI18nSubjectMatcher < BaseMatcher
@@ -10,7 +10,7 @@ module I18n::Tasks::Scanners::AstMatchers
10
10
  return unless children[1] == :default_i18n_subject
11
11
 
12
12
  key = @scanner.absolute_key(
13
- '.subject',
13
+ ".subject",
14
14
  location.expression.source_buffer.name,
15
15
  calling_method: method_name
16
16
  )
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/ast_matchers/base_matcher'
4
- require 'i18n/tasks/scanners/results/occurrence'
3
+ require "i18n/tasks/scanners/ast_matchers/base_matcher"
4
+ require "i18n/tasks/scanners/results/occurrence"
5
5
 
6
6
  module I18n::Tasks::Scanners::AstMatchers
7
7
  class MessageReceiversMatcher < BaseMatcher
@@ -64,25 +64,25 @@ module I18n::Tasks::Scanners::AstMatchers
64
64
  def process_options(node:, key:)
65
65
  return [key, nil] if node&.type != :hash
66
66
 
67
- scope_node = extract_hash_pair(node, 'scope')
67
+ scope_node = extract_hash_pair(node, "scope")
68
68
 
69
69
  if scope_node
70
70
  scope = extract_string(
71
71
  scope_node.children[1],
72
- array_join_with: '.',
72
+ array_join_with: ".",
73
73
  array_flatten: true,
74
74
  array_reject_blank: true
75
75
  )
76
76
  return nil if scope.nil? && scope_node.type != :nil
77
77
 
78
- key = [scope, key].join('.') unless scope == ''
78
+ key = [scope, key].join(".") unless scope == ""
79
79
  end
80
- if default_arg_node = extract_hash_pair(node, 'default')
80
+ if (default_arg_node = extract_hash_pair(node, "default"))
81
81
  default_arg = if default_arg_node.children[1]&.type == :hash
82
- extract_hash(default_arg_node.children[1])
83
- else
84
- extract_string(default_arg_node.children[1])
85
- end
82
+ extract_hash(default_arg_node.children[1])
83
+ else
84
+ extract_string(default_arg_node.children[1])
85
+ end
86
86
  end
87
87
 
88
88
  [key, default_arg]
@@ -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::AstMatchers
6
6
  class RailsModelMatcher < BaseMatcher
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/ruby_ast_scanner'
4
- require 'i18n/tasks/scanners/local_ruby_parser'
3
+ require "i18n/tasks/scanners/ruby_scanner"
4
+ require "i18n/tasks/scanners/local_ruby_parser"
5
+ require "i18n/tasks/scanners/occurrence_from_position"
6
+ require "prism"
5
7
 
6
8
  module I18n::Tasks::Scanners
7
- # Scan for I18n.translate calls in ERB-file better-html and ASTs
8
- class ErbAstScanner < RubyAstScanner
9
- DEFAULT_REGEXP = /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>/m.freeze
9
+ # Scan for I18n.translate calls in ERB-file using regexp and Parser/Prism
10
+ class ErbAstScanner < RubyScanner
11
+ include OccurrenceFromPosition
10
12
 
11
- def initialize(**args)
12
- super(**args)
13
- @ruby_parser = LocalRubyParser.new(ignore_blocks: true)
13
+ DEFAULT_REGEXP = /<%(={1,2}|-|\#-?|%)?(.*?)([-=])?%>/m
14
+
15
+ # Parser scanner, method called in RubyScanner
16
+ def ast_parser_parse_file(path)
17
+ @ruby_parser ||= LocalRubyParser.new(ignore_blocks: true)
18
+ super
14
19
  end
15
20
 
16
21
  private
@@ -34,11 +39,11 @@ module I18n::Tasks::Scanners
34
39
  stop = match.end(0) - 2 - (tailch&.size || 0)
35
40
 
36
41
  case character
37
- when '=', nil, '-'
42
+ when "=", nil, "-"
38
43
  parsed, parsed_comments = handle_code(buffer, code, start, stop)
39
44
  comments.concat(parsed_comments)
40
45
  children << parsed unless parsed.nil?
41
- when '#', '#-'
46
+ when "#", "#-"
42
47
  comments << handle_comment(buffer, start, stop)
43
48
  end
44
49
  end
@@ -74,5 +79,59 @@ module I18n::Tasks::Scanners
74
79
  )
75
80
  ::Parser::AST::Node.new(:erb, children, location: location)
76
81
  end
82
+
83
+ # Prism scanner, method called in RubyScanner
84
+ def prism_parse_file(path)
85
+ occurrences = []
86
+ content = File.read(path)
87
+
88
+ content.scan(DEFAULT_REGEXP) do |indicator, code, _tailch, _rspace|
89
+ match = Regexp.last_match
90
+ character = indicator ? indicator[0] : nil
91
+ start = match.begin(0) + 2 + (character&.size || 0)
92
+
93
+ case character
94
+ when "=", nil, "-"
95
+ occurrences += process_code(path, code, content, start)
96
+ when "#", "#-"
97
+ occurrences += process_comments(path, code, content, start)
98
+ end
99
+ end
100
+
101
+ occurrences
102
+ end
103
+
104
+ def process_code(path, code, content, start)
105
+ return [] if code.strip.empty? # skip empty ERB tags
106
+
107
+ process_prism_results(path, Prism.parse(code)).map do |key, occurrence|
108
+ [
109
+ key,
110
+ occurrence_from_position(
111
+ path,
112
+ content,
113
+ start + occurrence.pos,
114
+ raw_key: occurrence.raw_key
115
+ )
116
+ ]
117
+ end
118
+ end
119
+
120
+ def process_comments(path, code, content, start)
121
+ return [] if code.strip.empty?
122
+
123
+ parsed = Prism.parse(code.gsub("i18n-tasks-use ", "#i18n-tasks-use "))
124
+ process_prism_results(path, parsed).map do |key, occurrence|
125
+ [
126
+ key,
127
+ occurrence_from_position(
128
+ path,
129
+ content,
130
+ start + (code.index(key) || occurrence.pos),
131
+ raw_key: occurrence.raw_key
132
+ )
133
+ ]
134
+ end
135
+ end
77
136
  end
78
137
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/scanner'
3
+ require "i18n/tasks/scanners/scanner"
4
4
 
5
5
  module I18n::Tasks::Scanners
6
6
  # A base class for a scanner that analyses files.
@@ -16,7 +16,7 @@ module I18n::Tasks::Scanners
16
16
  file_reader: Files::CachingFileReader.new
17
17
  )
18
18
  super()
19
- @config = config
19
+ @config = config
20
20
  @file_reader = file_reader
21
21
  @file_finder = file_finder_provider.get(**config.slice(:paths, :only, :exclude))
22
22
  end
@@ -36,7 +36,7 @@ module I18n::Tasks::Scanners
36
36
  #
37
37
  # @return [Array<[key, Results::KeyOccurrence]>] each occurrence found in the file
38
38
  def scan_file(_path)
39
- fail 'Unimplemented'
39
+ fail "Unimplemented"
40
40
  end
41
41
 
42
42
  # Read a file. Reads of the same path are cached.
@@ -53,8 +53,8 @@ module I18n::Tasks::Scanners
53
53
  # @param (see FileFinder#traverse_files)
54
54
  # @yieldparam (see FileFinder#traverse_files)
55
55
  # @return (see FileFinder#traverse_files)
56
- def traverse_files(&block)
57
- @file_finder.traverse_files(&block)
56
+ def traverse_files(&)
57
+ @file_finder.traverse_files(&)
58
58
  end
59
59
 
60
60
  # @note This method is cached, it will only access the filesystem on the first invocation.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/concurrent/cached_value'
4
- require 'i18n/tasks/scanners/files/file_finder'
3
+ require "i18n/tasks/concurrent/cached_value"
4
+ require "i18n/tasks/scanners/files/file_finder"
5
5
 
6
6
  module I18n::Tasks::Scanners::Files
7
7
  # Finds the files in the specified search paths with support for exclusion / inclusion patterns.
@@ -23,7 +23,7 @@ module I18n::Tasks::Scanners::Files
23
23
  # @yieldparam (see FileFinder#traverse_files)
24
24
  # @return (see FileFinder#traverse_files)
25
25
 
26
- alias uncached_find_files find_files
26
+ alias_method :uncached_find_files, :find_files
27
27
  private :uncached_find_files
28
28
 
29
29
  # @note This method is cached, it will only access the filesystem on the first invocation.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/concurrent/cache'
4
- require 'i18n/tasks/scanners/files/caching_file_finder'
3
+ require "i18n/tasks/concurrent/cache"
4
+ require "i18n/tasks/scanners/files/caching_file_finder"
5
5
 
6
6
  module I18n::Tasks::Scanners::Files
7
7
  # Finds the files and provides their contents.
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners::Files
12
12
  # @param exclude [Array<String>]
13
13
  def initialize(exclude: [])
14
14
  @cache = ::I18n::Tasks::Concurrent::Cache.new
15
- @defaults = { exclude: exclude }
15
+ @defaults = {exclude: exclude}
16
16
  end
17
17
 
18
18
  # Initialize a {CachingFileFinder} or get one from cache based on the constructor arguments.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/concurrent/cache'
4
- require 'i18n/tasks/scanners/files/file_reader'
3
+ require "i18n/tasks/concurrent/cache"
4
+ require "i18n/tasks/scanners/files/file_reader"
5
5
 
6
6
  module I18n::Tasks::Scanners::Files
7
7
  # Reads the files in 'rb' mode and UTF-8 encoding.
@@ -13,10 +13,10 @@ module I18n::Tasks::Scanners::Files
13
13
  # Files not matching any of the inclusion patterns will be excluded.
14
14
  # @param exclude [Arry<String>] {File.fnmatch}-compatible patterns of files to exclude.
15
15
  # Files matching any of the exclusion patterns will be excluded even if they match an inclusion pattern.
16
- def initialize(paths: ['.'], only: nil, exclude: [])
17
- fail 'paths argument is required' if paths.nil?
16
+ def initialize(paths: ["."], only: nil, exclude: [])
17
+ fail "paths argument is required" if paths.nil?
18
18
 
19
- @paths = paths
19
+ @paths = paths
20
20
  @include = only
21
21
  @exclude = exclude || []
22
22
  end
@@ -26,8 +26,8 @@ module I18n::Tasks::Scanners::Files
26
26
  # @yield [path]
27
27
  # @yieldparam path [String] the path of the found file.
28
28
  # @return [Array<of block results>]
29
- def traverse_files(&block)
30
- find_files.map(&block)
29
+ def traverse_files(&)
30
+ find_files.map(&)
31
31
  end
32
32
 
33
33
  # @return [Array<String>] found files
@@ -36,10 +36,10 @@ module I18n::Tasks::Scanners::Files
36
36
  paths = @paths.select { |p| File.exist?(p) }
37
37
  log_warn "None of the search.paths exist #{@paths.inspect}" if paths.empty?
38
38
  Find.find(*paths) do |path|
39
- is_dir = File.directory?(path)
40
- hidden = File.basename(path).start_with?('.') && !%w[. ./].include?(path)
39
+ is_dir = File.directory?(path)
40
+ hidden = File.basename(path).start_with?(".") && !%w[. ./].include?(path)
41
41
  not_incl = @include && !path_fnmatch_any?(path, @include)
42
- excl = path_fnmatch_any?(path, @exclude)
42
+ excl = path_fnmatch_any?(path, @exclude)
43
43
  if is_dir || hidden || not_incl || excl
44
44
  Find.prune if is_dir && (hidden || excl)
45
45
  else
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners::Files
12
12
  # @return [String] file contents
13
13
  def read_file(path)
14
14
  result = nil
15
- File.open(path, 'rb', encoding: 'UTF-8') { |f| result = f.read }
15
+ File.open(path, "rb", encoding: "UTF-8") { |f| result = f.read }
16
16
  result
17
17
  end
18
18
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n/tasks/scanners/ruby_parser_factory'
3
+ require "i18n/tasks/scanners/ruby_parser_factory"
4
4
 
5
5
  module I18n::Tasks::Scanners
6
6
  class LocalRubyParser
7
7
  # ignore_blocks feature inspired by shopify/better-html
8
8
  # https://github.com/Shopify/better-html/blob/087943ffd2a5877fa977d71532010b0c91239519/lib/better_html/test_helper/ruby_node.rb#L24
9
- BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/.freeze
9
+ BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
10
10
 
11
11
  def initialize(ignore_blocks: false)
12
12
  @parser = RubyParserFactory.create_parser
@@ -15,12 +15,12 @@ module I18n::Tasks::Scanners
15
15
 
16
16
  # Parse string and normalize location
17
17
  def parse(source, location: nil)
18
- buffer = ::Parser::Source::Buffer.new('(string)')
18
+ buffer = ::Parser::Source::Buffer.new("(string)")
19
19
  buffer.source = if @ignore_blocks
20
- source.sub(BLOCK_EXPR, '')
21
- else
22
- source
23
- end
20
+ source.sub(BLOCK_EXPR, "")
21
+ else
22
+ source
23
+ end
24
24
 
25
25
  @parser.reset
26
26
  ast, comments = @parser.parse_with_comments(buffer)
@@ -42,7 +42,7 @@ module I18n::Tasks::Scanners
42
42
  node.updated(
43
43
  nil,
44
44
  node.children.map { |child| normalize_location(child, location) },
45
- { location: updated_location(location, node.location) }
45
+ {location: updated_location(location, node.location)}
46
46
  )
47
47
  end
48
48
 
@@ -13,7 +13,7 @@ module I18n
13
13
  # @return [Results::Occurrence]
14
14
  def occurrence_from_position(path, contents, position, raw_key: nil)
15
15
  line_begin = contents.rindex(/^/, position - 1)
16
- line_end = contents.index(/.(?=\r?\n|$)/, position)
16
+ line_end = contents.index(/.(?=\r?\n|$)/, position)
17
17
  Results::Occurrence.new(
18
18
  path: path,
19
19
  pos: position,