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.
- checksums.yaml +4 -4
- data/README.md +85 -13
- data/Rakefile +4 -4
- data/bin/i18n-tasks +3 -3
- data/config/locales/en.yml +6 -0
- data/config/locales/ru.yml +7 -0
- data/i18n-tasks.gemspec +28 -41
- data/lib/i18n/tasks/base_task.rb +19 -19
- data/lib/i18n/tasks/cli.rb +37 -30
- data/lib/i18n/tasks/command/collection.rb +4 -4
- data/lib/i18n/tasks/command/commander.rb +5 -5
- data/lib/i18n/tasks/command/commands/check_prism.rb +126 -0
- data/lib/i18n/tasks/command/commands/data.rb +33 -33
- data/lib/i18n/tasks/command/commands/eq_base.rb +3 -3
- data/lib/i18n/tasks/command/commands/health.rb +6 -5
- data/lib/i18n/tasks/command/commands/interpolations.rb +14 -3
- data/lib/i18n/tasks/command/commands/meta.rb +6 -6
- data/lib/i18n/tasks/command/commands/missing.rb +25 -25
- data/lib/i18n/tasks/command/commands/tree.rb +33 -33
- data/lib/i18n/tasks/command/commands/usages.rb +24 -24
- data/lib/i18n/tasks/command/dsl.rb +1 -1
- data/lib/i18n/tasks/command/option_parsers/enum.rb +5 -5
- data/lib/i18n/tasks/command/option_parsers/locale.rb +4 -4
- data/lib/i18n/tasks/command/options/common.rb +16 -16
- data/lib/i18n/tasks/command/options/data.rb +18 -18
- data/lib/i18n/tasks/command/options/locales.rb +32 -32
- data/lib/i18n/tasks/commands.rb +14 -12
- data/lib/i18n/tasks/concurrent/cache.rb +1 -1
- data/lib/i18n/tasks/concurrent/cached_value.rb +1 -1
- data/lib/i18n/tasks/configuration.rb +22 -21
- data/lib/i18n/tasks/console_context.rb +11 -11
- data/lib/i18n/tasks/data/adapter/json_adapter.rb +1 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +5 -5
- data/lib/i18n/tasks/data/file_formats.rb +3 -3
- data/lib/i18n/tasks/data/file_system.rb +5 -5
- data/lib/i18n/tasks/data/file_system_base.rb +26 -26
- data/lib/i18n/tasks/data/language_names.rb +202 -0
- data/lib/i18n/tasks/data/router/conservative_router.rb +3 -3
- data/lib/i18n/tasks/data/router/isolating_router.rb +19 -19
- data/lib/i18n/tasks/data/router/pattern_router.rb +5 -5
- data/lib/i18n/tasks/data/tree/node.rb +27 -27
- data/lib/i18n/tasks/data/tree/nodes.rb +10 -10
- data/lib/i18n/tasks/data/tree/siblings.rb +20 -20
- data/lib/i18n/tasks/data/tree/traversal.rb +5 -5
- data/lib/i18n/tasks/data.rb +4 -4
- data/lib/i18n/tasks/html_keys.rb +2 -2
- data/lib/i18n/tasks/ignore_keys.rb +9 -9
- data/lib/i18n/tasks/interpolations.rb +21 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +8 -8
- data/lib/i18n/tasks/logging.rb +2 -1
- data/lib/i18n/tasks/missing_keys.rb +24 -8
- data/lib/i18n/tasks/plural_keys.rb +6 -4
- data/lib/i18n/tasks/references.rb +4 -4
- data/lib/i18n/tasks/reports/base.rb +18 -14
- data/lib/i18n/tasks/reports/terminal.rb +64 -47
- data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +3 -3
- data/lib/i18n/tasks/scanners/ast_matchers/default_i18n_subject_matcher.rb +3 -3
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +10 -10
- data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +1 -1
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +70 -10
- data/lib/i18n/tasks/scanners/file_scanner.rb +5 -5
- data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +3 -3
- data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +3 -3
- data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +2 -2
- data/lib/i18n/tasks/scanners/files/file_finder.rb +8 -8
- data/lib/i18n/tasks/scanners/files/file_reader.rb +1 -1
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +8 -8
- data/lib/i18n/tasks/scanners/occurrence_from_position.rb +4 -3
- data/lib/i18n/tasks/scanners/pattern_mapper.rb +7 -7
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +20 -20
- data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +8 -8
- data/lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb +8 -1
- data/lib/i18n/tasks/scanners/prism_scanners/nodes.rb +123 -62
- data/lib/i18n/tasks/scanners/prism_scanners/visitor.rb +186 -105
- data/lib/i18n/tasks/scanners/relative_keys.rb +8 -8
- data/lib/i18n/tasks/scanners/results/key_occurrences.rb +3 -3
- data/lib/i18n/tasks/scanners/results/occurrence.rb +14 -10
- data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +6 -6
- data/lib/i18n/tasks/scanners/ruby_parser_factory.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_scanner.rb +226 -0
- data/lib/i18n/tasks/scanners/scanner.rb +2 -2
- data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +3 -24
- data/lib/i18n/tasks/split_key.rb +4 -4
- data/lib/i18n/tasks/stats.rb +3 -3
- data/lib/i18n/tasks/translation.rb +5 -5
- data/lib/i18n/tasks/translators/base_translator.rb +40 -14
- data/lib/i18n/tasks/translators/deepl_translator.rb +17 -14
- data/lib/i18n/tasks/translators/google_translator.rb +169 -25
- data/lib/i18n/tasks/translators/openai_translator.rb +34 -23
- data/lib/i18n/tasks/translators/watsonx_translator.rb +16 -16
- data/lib/i18n/tasks/translators/yandex_translator.rb +8 -8
- data/lib/i18n/tasks/unused_keys.rb +1 -1
- data/lib/i18n/tasks/used_keys.rb +32 -33
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/i18n/tasks.rb +17 -17
- data/templates/config/i18n-tasks.yml +12 -0
- data/templates/minitest/i18n_test.rb +3 -3
- data/templates/rspec/i18n_spec.rb +7 -7
- metadata +25 -185
- data/lib/i18n/tasks/scanners/prism_scanner.rb +0 -83
- 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
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
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
|
-
#
|
|
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
|
|
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(
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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$/,
|
|
61
|
+
"#{file_key.sub(/_controller$/, "")}.#{calling_method}"
|
|
62
62
|
else
|
|
63
63
|
# Remove _ prefix from partials
|
|
64
|
-
file_key.gsub(
|
|
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
|
|
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
|
|
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
|
|
41
|
-
@pos
|
|
42
|
-
@line_num
|
|
43
|
-
@line_pos
|
|
44
|
-
@line
|
|
45
|
-
@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]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module I18n::Tasks::Scanners
|
|
4
4
|
module RubyKeyLiterals
|
|
5
|
-
LITERAL_RE = /:?".+?"|:?'.+?'|:\w
|
|
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
|
|
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]))}
|
|
24
|
-
VALID_KEY_RE
|
|
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
|