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