i18n-tasks 1.0.14 → 1.0.15
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.
- checksums.yaml +4 -4
- data/README.md +65 -38
- data/config/locales/en.yml +11 -1
- data/config/locales/ru.yml +11 -1
- data/i18n-tasks.gemspec +4 -1
- data/lib/i18n/tasks/command/commands/missing.rb +3 -1
- data/lib/i18n/tasks/command/option_parsers/enum.rb +4 -3
- data/lib/i18n/tasks/command/options/locales.rb +12 -3
- data/lib/i18n/tasks/configuration.rb +7 -2
- data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +1 -1
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +2 -2
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +1 -1
- data/lib/i18n/tasks/scanners/prism_scanner.rb +83 -0
- data/lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb +41 -0
- data/lib/i18n/tasks/scanners/prism_scanners/nodes.rb +334 -0
- data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +273 -0
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +3 -3
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_parser_factory.rb +27 -0
- data/lib/i18n/tasks/translation.rb +4 -1
- data/lib/i18n/tasks/translators/base_translator.rb +5 -1
- data/lib/i18n/tasks/translators/deepl_translator.rb +5 -0
- data/lib/i18n/tasks/translators/google_translator.rb +12 -4
- data/lib/i18n/tasks/translators/openai_translator.rb +34 -20
- data/lib/i18n/tasks/translators/watsonx_translator.rb +155 -0
- data/lib/i18n/tasks/translators/yandex_translator.rb +5 -1
- data/lib/i18n/tasks/used_keys.rb +3 -2
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/i18n/tasks.rb +1 -0
- data/templates/config/i18n-tasks.yml +2 -2
- metadata +31 -5
@@ -0,0 +1,334 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# These classes are used in the PrismScanners::Visitor class to store the translations found in the parsed code
|
4
|
+
# Used in the PrismScanners::Visitor class.
|
5
|
+
module I18n::Tasks::Scanners::PrismScanners
|
6
|
+
class Root
|
7
|
+
attr_reader(:calls, :translation_calls, :children, :node, :parent)
|
8
|
+
|
9
|
+
def initialize(node: nil, parent: nil)
|
10
|
+
@calls = []
|
11
|
+
@translation_calls = []
|
12
|
+
@children = []
|
13
|
+
@node = node
|
14
|
+
@parent = parent
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_child(node)
|
18
|
+
@children << node
|
19
|
+
node
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_call(node)
|
23
|
+
@calls << node
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_translation_call(translation_call)
|
27
|
+
@translation_calls << translation_call
|
28
|
+
end
|
29
|
+
|
30
|
+
def support_relative_keys?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def private_method
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def path
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
|
42
|
+
def process
|
43
|
+
(@translation_calls + @children.flat_map(&:process)).flatten
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class TranslationCall
|
48
|
+
attr_reader(:node, :key, :receiver, :options, :parent)
|
49
|
+
|
50
|
+
def initialize(node:, key:, receiver:, options:, parent:)
|
51
|
+
@node = node
|
52
|
+
@key = key
|
53
|
+
@receiver = receiver
|
54
|
+
@options = options
|
55
|
+
@parent = parent
|
56
|
+
end
|
57
|
+
|
58
|
+
def relative_key?
|
59
|
+
@key&.start_with?('.') && @receiver.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
def with_parent(parent)
|
63
|
+
self.class.new(
|
64
|
+
node: @node,
|
65
|
+
key: @key,
|
66
|
+
receiver: @receiver,
|
67
|
+
options: @options,
|
68
|
+
parent: parent
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def with_node(node)
|
73
|
+
self.class.new(
|
74
|
+
node: node,
|
75
|
+
key: @key,
|
76
|
+
receiver: @receiver,
|
77
|
+
options: @options,
|
78
|
+
parent: @parent
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def occurrences(file_path)
|
83
|
+
occurrence(file_path)
|
84
|
+
end
|
85
|
+
|
86
|
+
def full_key
|
87
|
+
return nil if key.nil?
|
88
|
+
return nil unless key.is_a?(String)
|
89
|
+
return nil if relative_key? && !support_relative_keys?
|
90
|
+
|
91
|
+
parts = [scope]
|
92
|
+
|
93
|
+
if relative_key?
|
94
|
+
parts.concat(parent&.path || [])
|
95
|
+
parts << key
|
96
|
+
|
97
|
+
# TODO: Fallback to controller without action name
|
98
|
+
elsif key.start_with?('.')
|
99
|
+
parts << key[1..]
|
100
|
+
else
|
101
|
+
parts << key
|
102
|
+
end
|
103
|
+
|
104
|
+
parts.compact.join('.').gsub('..', '.')
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def scope
|
110
|
+
return nil if @options.nil?
|
111
|
+
return nil unless @options['scope']
|
112
|
+
|
113
|
+
Array(@options['scope']).compact.map(&:to_s).join('.')
|
114
|
+
end
|
115
|
+
|
116
|
+
def occurrence(file_path)
|
117
|
+
local_node = @node
|
118
|
+
|
119
|
+
location = local_node.location
|
120
|
+
|
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)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Only public methods are added to the context path
|
150
|
+
# Only some classes supports relative keys
|
151
|
+
def support_relative_keys?
|
152
|
+
parent.is_a?(ParsedMethod) && parent.support_relative_keys?
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class ParsedModule < Root
|
157
|
+
def support_relative_keys?
|
158
|
+
false
|
159
|
+
end
|
160
|
+
|
161
|
+
def private_method
|
162
|
+
false
|
163
|
+
end
|
164
|
+
|
165
|
+
def path
|
166
|
+
(@parent&.path || []) + [path_name]
|
167
|
+
end
|
168
|
+
|
169
|
+
def path_name
|
170
|
+
@node.name.to_s.underscore
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class ParsedClass < Root
|
175
|
+
attr_reader(:private_method)
|
176
|
+
|
177
|
+
def initialize(node:, parent:, rails:)
|
178
|
+
@private_method = false
|
179
|
+
@methods = []
|
180
|
+
@private_methods = []
|
181
|
+
@before_actions = []
|
182
|
+
@rails = rails
|
183
|
+
|
184
|
+
super(node: node, parent: parent)
|
185
|
+
end
|
186
|
+
|
187
|
+
def add_child(node)
|
188
|
+
case node
|
189
|
+
when ParsedMethod
|
190
|
+
if @private_method
|
191
|
+
@private_methods << node
|
192
|
+
else
|
193
|
+
@methods << node
|
194
|
+
end
|
195
|
+
when ParsedBeforeAction
|
196
|
+
@before_actions << node
|
197
|
+
end
|
198
|
+
|
199
|
+
super
|
200
|
+
end
|
201
|
+
|
202
|
+
def process # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
203
|
+
return @children.flat_map(&:process) unless controller?
|
204
|
+
|
205
|
+
methods_by_name = @methods.group_by(&:name)
|
206
|
+
private_methods_by_name = @private_methods.group_by(&:name)
|
207
|
+
|
208
|
+
# For each before_action we need to
|
209
|
+
# - Find which method it calls
|
210
|
+
# - Find out which methods it applies to
|
211
|
+
# - Calculate translation calls (and see if they are relative)
|
212
|
+
# - Add the translation calls to the methods it applies to
|
213
|
+
|
214
|
+
@before_actions.each do |before_action|
|
215
|
+
before_action_name = before_action.name&.to_sym
|
216
|
+
method_call = methods_by_name[before_action_name]&.first || private_methods_by_name[before_action_name]&.first
|
217
|
+
translation_calls = (method_call&.translation_calls || []) + before_action.translation_calls
|
218
|
+
|
219
|
+
# We need to handle the parent here, should not be the before_action when it is called in the method.
|
220
|
+
@methods.each do |method|
|
221
|
+
next unless before_action.applies_to?(method.name)
|
222
|
+
|
223
|
+
method.add_translation_call(
|
224
|
+
translation_calls.map { |call| call.with_parent(method) }
|
225
|
+
)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
nested_calls = {}
|
230
|
+
new_translation_calls = []
|
231
|
+
|
232
|
+
@methods.each do |method|
|
233
|
+
method.calls.each do |call|
|
234
|
+
next if call.receiver.present?
|
235
|
+
|
236
|
+
other_method = methods_by_name[call.name]&.first || private_methods_by_name[call.name]&.first
|
237
|
+
next unless other_method
|
238
|
+
|
239
|
+
nested_calls[method.name] ||= []
|
240
|
+
nested_calls[method.name] << other_method.name
|
241
|
+
|
242
|
+
if nested_calls[call.name]&.include?(method.name)
|
243
|
+
fail(ArgumentError, "Cyclic call detected: #{call.name} -> #{method.name}")
|
244
|
+
end
|
245
|
+
|
246
|
+
other_method.translation_calls.each do |translation_call|
|
247
|
+
new_translation_calls.push(translation_call.with_parent(method))
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
@children.flat_map(&:process) + new_translation_calls
|
253
|
+
end
|
254
|
+
|
255
|
+
def private_methods!
|
256
|
+
@private_method = true
|
257
|
+
end
|
258
|
+
|
259
|
+
def support_relative_keys?
|
260
|
+
@rails && controller?
|
261
|
+
end
|
262
|
+
|
263
|
+
def path
|
264
|
+
(@parent&.path || []) + [path_name]
|
265
|
+
end
|
266
|
+
|
267
|
+
def controller?
|
268
|
+
@node.name.to_s.end_with?('Controller')
|
269
|
+
end
|
270
|
+
|
271
|
+
def path_name
|
272
|
+
path = @node.constant_path.full_name_parts.map { |s| s.to_s.underscore }
|
273
|
+
path.last.gsub!(/_controller\z/, '') if controller?
|
274
|
+
|
275
|
+
path
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class ParsedMethod < Root
|
280
|
+
def initialize(node:, parent:, private_method: false)
|
281
|
+
@private_method = private_method
|
282
|
+
|
283
|
+
super(node: node, parent: parent)
|
284
|
+
end
|
285
|
+
|
286
|
+
def support_relative_keys?
|
287
|
+
!@private_method && @parent&.support_relative_keys?
|
288
|
+
end
|
289
|
+
|
290
|
+
def path
|
291
|
+
(@parent&.path || []) + [@node.name]
|
292
|
+
end
|
293
|
+
|
294
|
+
def name
|
295
|
+
@node.name
|
296
|
+
end
|
297
|
+
|
298
|
+
def process
|
299
|
+
@translation_calls
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
class ParsedBeforeAction < Root
|
304
|
+
attr_reader(:name)
|
305
|
+
|
306
|
+
def initialize(node:, parent:, name: nil, only: nil, except: nil)
|
307
|
+
@name = name
|
308
|
+
@only = only.present? ? Array(only).map(&:to_s) : nil
|
309
|
+
@except = except.present? ? Array(except).map(&:to_s) : nil
|
310
|
+
|
311
|
+
super(node: node, parent: parent)
|
312
|
+
end
|
313
|
+
|
314
|
+
def support_relative_keys?
|
315
|
+
true
|
316
|
+
end
|
317
|
+
|
318
|
+
def applies_to?(method_name)
|
319
|
+
if @only.nil? && @except.nil?
|
320
|
+
true
|
321
|
+
elsif @only.nil?
|
322
|
+
!@except.include?(method_name.to_s)
|
323
|
+
elsif @except.nil?
|
324
|
+
@only.include?(method_name.to_s)
|
325
|
+
else
|
326
|
+
false
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def path
|
331
|
+
@parent&.path || []
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism/visitor'
|
4
|
+
require_relative 'nodes'
|
5
|
+
require_relative 'arguments_visitor'
|
6
|
+
|
7
|
+
# Implementation of Prism::Visitor (https://ruby.github.io/prism/rb/Prism/Visitor.html)
|
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
|
+
# 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.
|
12
|
+
|
13
|
+
module I18n::Tasks::Scanners::PrismScanners
|
14
|
+
class Visitor < Prism::Visitor # rubocop:disable Metrics/ClassLength
|
15
|
+
MAGIC_COMMENT_PREFIX = /\A.\s*i18n-tasks-use\s+/.freeze
|
16
|
+
|
17
|
+
attr_reader(:calls, :current_module, :current_class, :current_method, :root)
|
18
|
+
|
19
|
+
def initialize(comments: nil, rails: false)
|
20
|
+
@calls = []
|
21
|
+
@comment_translations_by_row = prepare_comments_by_line(comments)
|
22
|
+
|
23
|
+
@current_module = nil
|
24
|
+
@current_class = nil
|
25
|
+
@current_method = nil
|
26
|
+
@root = Root.new
|
27
|
+
|
28
|
+
@rails = rails
|
29
|
+
|
30
|
+
# Needs to have () because the Prism::Visitor has no arguments
|
31
|
+
super()
|
32
|
+
end
|
33
|
+
|
34
|
+
def parent
|
35
|
+
@current_before_action || @current_method || @current_class || @current_module || @root
|
36
|
+
end
|
37
|
+
|
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
|
+
def visit_module_node(node)
|
69
|
+
previous_module = @current_module
|
70
|
+
@current_module = parent.add_child(
|
71
|
+
ParsedModule.new(node: node, parent: parent)
|
72
|
+
)
|
73
|
+
|
74
|
+
super
|
75
|
+
ensure
|
76
|
+
@current_module = previous_module
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_class_node(node)
|
80
|
+
previous_class = @current_class
|
81
|
+
|
82
|
+
@current_class = parent.add_child(
|
83
|
+
ParsedClass.new(
|
84
|
+
node: node,
|
85
|
+
parent: parent,
|
86
|
+
rails: @rails
|
87
|
+
)
|
88
|
+
)
|
89
|
+
|
90
|
+
super
|
91
|
+
ensure
|
92
|
+
@current_class = previous_class
|
93
|
+
end
|
94
|
+
|
95
|
+
def visit_def_node(node)
|
96
|
+
previous_method = @current_method
|
97
|
+
parent = @current_class || @current_module || @root
|
98
|
+
@current_method = parent.add_child(
|
99
|
+
ParsedMethod.new(
|
100
|
+
node: node,
|
101
|
+
parent: parent,
|
102
|
+
private_method: parent.private_method
|
103
|
+
)
|
104
|
+
)
|
105
|
+
|
106
|
+
super
|
107
|
+
ensure
|
108
|
+
@current_method = previous_method
|
109
|
+
end
|
110
|
+
|
111
|
+
def visit_call_node(node)
|
112
|
+
handle_comments(node)
|
113
|
+
|
114
|
+
case node.name
|
115
|
+
when :private
|
116
|
+
@current_class&.private_methods!
|
117
|
+
when :t, :t!, :translate, :translate!
|
118
|
+
|
119
|
+
args, kwargs = process_arguments(node)
|
120
|
+
parent.add_translation_call(
|
121
|
+
TranslationCall.new(
|
122
|
+
node: node,
|
123
|
+
key: args[0],
|
124
|
+
receiver: node.receiver,
|
125
|
+
options: kwargs,
|
126
|
+
parent: parent
|
127
|
+
)
|
128
|
+
)
|
129
|
+
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
|
137
|
+
end
|
138
|
+
|
139
|
+
super
|
140
|
+
end
|
141
|
+
|
142
|
+
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
|
+
@root.process
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def process_arguments(node)
|
154
|
+
return [], {} if node.nil?
|
155
|
+
return [], {} unless node.respond_to?(:arguments)
|
156
|
+
return [], {} if node.arguments.nil?
|
157
|
+
|
158
|
+
arguments_visitor = ArgumentsVisitor.new
|
159
|
+
arguments = node.arguments.accept(arguments_visitor)
|
160
|
+
keywords, args = arguments.partition { |arg| arg.is_a?(Hash) }
|
161
|
+
|
162
|
+
[args.compact, keywords.first || {}]
|
163
|
+
end
|
164
|
+
|
165
|
+
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)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# ---- Rails specific methods ----
|
174
|
+
# Returns true if the node was handled
|
175
|
+
def rails_call_node(node, &block)
|
176
|
+
case node.name
|
177
|
+
when :before_action
|
178
|
+
rails_handle_before_action(node, &block)
|
179
|
+
true
|
180
|
+
when :human_attribute_name
|
181
|
+
rails_handle_human_attribute_name(node)
|
182
|
+
true
|
183
|
+
when :human
|
184
|
+
return false if node.receiver.name != :model_name
|
185
|
+
|
186
|
+
rails_handle_model_name(node)
|
187
|
+
true
|
188
|
+
else
|
189
|
+
false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def rails_handle_before_action(node) # rubocop:disable Metrics/MethodLength
|
194
|
+
array_arguments, keywords = process_arguments(node)
|
195
|
+
first_argument = array_arguments.first
|
196
|
+
|
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)
|
224
|
+
|
225
|
+
yield
|
226
|
+
ensure
|
227
|
+
@current_before_action = nil
|
228
|
+
end
|
229
|
+
|
230
|
+
def rails_handle_model_name(node)
|
231
|
+
_args, kwargs = process_arguments(node)
|
232
|
+
model_name = node.receiver.receiver.name.to_s.underscore
|
233
|
+
|
234
|
+
count_key = (kwargs['count'] || 0) > 1 ? 'other' : 'one'
|
235
|
+
parent.add_translation_call(
|
236
|
+
TranslationCall.new(
|
237
|
+
node: node,
|
238
|
+
receiver: nil,
|
239
|
+
key: [:activerecord, :models, model_name, count_key].join('.'),
|
240
|
+
parent: parent,
|
241
|
+
options: kwargs
|
242
|
+
)
|
243
|
+
)
|
244
|
+
end
|
245
|
+
|
246
|
+
def rails_handle_human_attribute_name(node)
|
247
|
+
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
|
+
)
|
253
|
+
end
|
254
|
+
|
255
|
+
key = [
|
256
|
+
:activerecord,
|
257
|
+
:attributes,
|
258
|
+
node.receiver.name.to_s.underscore,
|
259
|
+
array_args.first
|
260
|
+
].join('.')
|
261
|
+
|
262
|
+
parent.add_translation_call(
|
263
|
+
TranslationCall.new(
|
264
|
+
node: node,
|
265
|
+
key: key,
|
266
|
+
receiver: nil,
|
267
|
+
parent: parent,
|
268
|
+
options: {}
|
269
|
+
)
|
270
|
+
)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -3,10 +3,10 @@
|
|
3
3
|
require 'i18n/tasks/scanners/file_scanner'
|
4
4
|
require 'i18n/tasks/scanners/relative_keys'
|
5
5
|
require 'i18n/tasks/scanners/ruby_ast_call_finder'
|
6
|
+
require 'i18n/tasks/scanners/ruby_parser_factory'
|
6
7
|
require 'i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher'
|
7
8
|
require 'i18n/tasks/scanners/ast_matchers/message_receivers_matcher'
|
8
9
|
require 'i18n/tasks/scanners/ast_matchers/rails_model_matcher'
|
9
|
-
require 'parser/current'
|
10
10
|
|
11
11
|
module I18n::Tasks::Scanners
|
12
12
|
# Scan for I18n.translate calls using whitequark/parser
|
@@ -18,8 +18,8 @@ module I18n::Tasks::Scanners
|
|
18
18
|
|
19
19
|
def initialize(**args)
|
20
20
|
super(**args)
|
21
|
-
@parser =
|
22
|
-
@magic_comment_parser =
|
21
|
+
@parser = RubyParserFactory.create_parser
|
22
|
+
@magic_comment_parser = RubyParserFactory.create_parser
|
23
23
|
@matchers = setup_matchers
|
24
24
|
end
|
25
25
|
|
@@ -20,7 +20,7 @@ module I18n::Tasks::Scanners
|
|
20
20
|
literal
|
21
21
|
end
|
22
22
|
|
23
|
-
VALID_KEY_CHARS = %r{(?:[[:word:]]|[
|
23
|
+
VALID_KEY_CHARS = %r{(?:[[:word:]]|[-.?!:;À-ž\\/]|(?<=[\p{L}\d])\s(?=[\p{L}\d]))}.freeze
|
24
24
|
VALID_KEY_RE = /^#{VALID_KEY_CHARS}+$/.freeze
|
25
25
|
|
26
26
|
def valid_key?(key)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module provides a factory class for creating a Ruby parser instance.
|
4
|
+
# It temporarily disables verbose mode to suppress compatibility warnings
|
5
|
+
# when loading the "parser/current" library.
|
6
|
+
#
|
7
|
+
# Example warning for the release of Ruby 3.4.1:
|
8
|
+
# warning: parser/current is loading parser/ruby34, which recognizes
|
9
|
+
# 3.4.0-compliant syntax, but you are running 3.4.1.
|
10
|
+
# Please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
|
11
|
+
#
|
12
|
+
# By disabling verbose mode, these warnings are suppressed to provide a cleaner
|
13
|
+
# output and avoid confusion. The verbose mode is restored after the parser
|
14
|
+
# instance is created to maintain the original behavior.
|
15
|
+
|
16
|
+
module I18n::Tasks::Scanners
|
17
|
+
class RubyParserFactory
|
18
|
+
def self.create_parser
|
19
|
+
prev = $VERBOSE
|
20
|
+
$VERBOSE = nil
|
21
|
+
require 'parser/current'
|
22
|
+
::Parser::CurrentRuby.new
|
23
|
+
ensure
|
24
|
+
$VERBOSE = prev
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'i18n/tasks/translators/deepl_translator'
|
4
4
|
require 'i18n/tasks/translators/google_translator'
|
5
5
|
require 'i18n/tasks/translators/openai_translator'
|
6
|
+
require 'i18n/tasks/translators/watsonx_translator'
|
6
7
|
require 'i18n/tasks/translators/yandex_translator'
|
7
8
|
|
8
9
|
module I18n::Tasks
|
@@ -11,7 +12,7 @@ module I18n::Tasks
|
|
11
12
|
# @param [String] from locale
|
12
13
|
# @param [:deepl, :openai, :google, :yandex] backend
|
13
14
|
# @return [I18n::Tasks::Tree::Siblings] translated forest
|
14
|
-
def translate_forest(forest, from:, backend:
|
15
|
+
def translate_forest(forest, from:, backend:)
|
15
16
|
case backend
|
16
17
|
when :deepl
|
17
18
|
Translators::DeeplTranslator.new(self).translate_forest(forest, from)
|
@@ -19,6 +20,8 @@ module I18n::Tasks
|
|
19
20
|
Translators::GoogleTranslator.new(self).translate_forest(forest, from)
|
20
21
|
when :openai
|
21
22
|
Translators::OpenAiTranslator.new(self).translate_forest(forest, from)
|
23
|
+
when :watsonx
|
24
|
+
Translators::WatsonxTranslator.new(self).translate_forest(forest, from)
|
22
25
|
when :yandex
|
23
26
|
Translators::YandexTranslator.new(self).translate_forest(forest, from)
|
24
27
|
else
|
@@ -14,7 +14,11 @@ module I18n::Tasks
|
|
14
14
|
# @return [I18n::Tasks::Tree::Siblings] translated forest
|
15
15
|
def translate_forest(forest, from)
|
16
16
|
forest.inject @i18n_tasks.empty_forest do |result, root|
|
17
|
-
|
17
|
+
pairs = root.key_values(root: true)
|
18
|
+
|
19
|
+
@progress_bar = ProgressBar.create(total: pairs.flatten.size, format: '%a <%B> %e %c/%C (%p%%)')
|
20
|
+
|
21
|
+
translated = translate_pairs(pairs, to: root.key, from: from)
|
18
22
|
result.merge! Data::Tree::Siblings.from_flat_pairs(translated)
|
19
23
|
end
|
20
24
|
end
|