i18n-tasks 0.9.37 → 1.0.13
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 +43 -5
- data/Rakefile +1 -1
- data/config/locales/en.yml +12 -3
- data/config/locales/ru.yml +8 -0
- data/i18n-tasks.gemspec +14 -7
- data/lib/i18n/tasks/cli.rb +8 -7
- data/lib/i18n/tasks/command/commander.rb +1 -0
- data/lib/i18n/tasks/command/commands/missing.rb +17 -5
- data/lib/i18n/tasks/command/options/common.rb +0 -1
- data/lib/i18n/tasks/command/options/data.rb +1 -1
- data/lib/i18n/tasks/concurrent/cached_value.rb +0 -2
- data/lib/i18n/tasks/configuration.rb +13 -7
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +15 -2
- data/lib/i18n/tasks/data/file_formats.rb +1 -1
- data/lib/i18n/tasks/data/router/pattern_router.rb +1 -1
- data/lib/i18n/tasks/data/tree/node.rb +1 -1
- data/lib/i18n/tasks/data/tree/nodes.rb +5 -7
- data/lib/i18n/tasks/data/tree/siblings.rb +1 -2
- data/lib/i18n/tasks/data/tree/traversal.rb +25 -11
- data/lib/i18n/tasks/html_keys.rb +2 -2
- data/lib/i18n/tasks/interpolations.rb +7 -3
- data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
- data/lib/i18n/tasks/locale_pathname.rb +1 -1
- data/lib/i18n/tasks/plural_keys.rb +0 -6
- data/lib/i18n/tasks/references.rb +3 -3
- data/lib/i18n/tasks/reports/base.rb +2 -2
- data/lib/i18n/tasks/reports/terminal.rb +3 -3
- data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +118 -0
- data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +91 -0
- data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +69 -0
- data/lib/i18n/tasks/scanners/erb_ast_processor.rb +74 -0
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
- data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -2
- data/lib/i18n/tasks/scanners/relative_keys.rb +2 -2
- data/lib/i18n/tasks/scanners/results/occurrence.rb +17 -1
- data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +9 -34
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +91 -156
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +2 -2
- data/lib/i18n/tasks/split_key.rb +1 -1
- data/lib/i18n/tasks/translation.rb +4 -1
- data/lib/i18n/tasks/translators/base_translator.rb +17 -12
- data/lib/i18n/tasks/translators/deepl_translator.rb +34 -11
- data/lib/i18n/tasks/translators/google_translator.rb +1 -1
- data/lib/i18n/tasks/translators/openai_translator.rb +100 -0
- data/lib/i18n/tasks/used_keys.rb +9 -6
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/i18n/tasks.rb +11 -0
- data/templates/config/i18n-tasks.yml +17 -4
- data/templates/minitest/i18n_test.rb +6 -6
- metadata +74 -16
@@ -52,15 +52,9 @@ module I18n::Tasks::PluralKeys
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def plural_forms?(s)
|
55
|
-
return false if non_plural_other?(s)
|
56
|
-
|
57
55
|
s.present? && s.all? { |node| node.leaf? && plural_suffix?(node.key) }
|
58
56
|
end
|
59
57
|
|
60
|
-
def non_plural_other?(s)
|
61
|
-
s.size == 1 && s.first.leaf? && (!s.first.value.is_a?(String) || !s.first.value.include?('%{count}'))
|
62
|
-
end
|
63
|
-
|
64
58
|
def plural_suffix?(key)
|
65
59
|
PLURAL_KEY_SUFFIXES.include?(key)
|
66
60
|
end
|
@@ -90,9 +90,9 @@ module I18n::Tasks
|
|
90
90
|
on_leaves_merge: lambda do |node, other|
|
91
91
|
if node.value != other.value
|
92
92
|
log_warn(
|
93
|
-
'Conflicting references: '\
|
94
|
-
|
95
|
-
|
93
|
+
'Conflicting references: ' \
|
94
|
+
"#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]}, " \
|
95
|
+
"but ⮕ #{other.value} in #{other.data[:locale]}"
|
96
96
|
)
|
97
97
|
end
|
98
98
|
end
|
@@ -36,8 +36,8 @@ module I18n::Tasks::Reports
|
|
36
36
|
|
37
37
|
def used_title(keys_nodes, filter)
|
38
38
|
used_n = keys_nodes.map { |_k, node| node.data[:occurrences].size }.reduce(:+).to_i
|
39
|
-
"#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}"\
|
40
|
-
|
39
|
+
"#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}" \
|
40
|
+
"#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
|
41
41
|
end
|
42
42
|
|
43
43
|
# Sort keys by their attributes in order
|
@@ -112,7 +112,7 @@ module I18n
|
|
112
112
|
when :missing_plural
|
113
113
|
leaf[:data][:missing_keys].join(', ')
|
114
114
|
else
|
115
|
-
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} "\
|
115
|
+
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} " \
|
116
116
|
"#{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
|
117
117
|
end
|
118
118
|
end
|
@@ -121,9 +121,9 @@ module I18n
|
|
121
121
|
if data[:ref_info]
|
122
122
|
from, to = data[:ref_info]
|
123
123
|
resolved = key[0...to.length]
|
124
|
-
after = key[to.length
|
124
|
+
after = key[to.length..]
|
125
125
|
" #{Rainbow(from).yellow}#{Rainbow(after).cyan}\n" \
|
126
|
-
|
126
|
+
"#{Rainbow('⮕').yellow.bright} #{Rainbow(resolved).yellow.bright}"
|
127
127
|
else
|
128
128
|
Rainbow(key).cyan
|
129
129
|
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks::Scanners::AstMatchers
|
4
|
+
class BaseMatcher
|
5
|
+
def initialize(scanner:)
|
6
|
+
@scanner = scanner
|
7
|
+
end
|
8
|
+
|
9
|
+
def convert_to_key_occurrences(send_node, _method_name, location: send_node.loc)
|
10
|
+
fail('Not implemented')
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# If the node type is of `%i(sym str int false true)`, return the value as a string.
|
16
|
+
# Otherwise, if `config[:strict]` is `false` and the type is of `%i(dstr dsym)`,
|
17
|
+
# return the source as if it were a string.
|
18
|
+
#
|
19
|
+
# @param node [Parser::AST::Node]
|
20
|
+
# @param array_join_with [String, nil] if set to a string, arrays will be processed and their elements joined.
|
21
|
+
# @param array_flatten [Boolean] if true, nested arrays are flattened,
|
22
|
+
# otherwise their source is copied and surrounded by #{}. No effect unless `array_join_with` is set.
|
23
|
+
# @param array_reject_blank [Boolean] if true, empty strings and `nil`s are skipped.
|
24
|
+
# No effect unless `array_join_with` is set.
|
25
|
+
# @return [String, nil] `nil` is returned only when a dynamic value is encountered in strict mode
|
26
|
+
# or the node type is not supported.
|
27
|
+
def extract_string(node, array_join_with: nil, array_flatten: false, array_reject_blank: false) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
28
|
+
return if node.nil?
|
29
|
+
|
30
|
+
if %i[sym str int].include?(node.type)
|
31
|
+
node.children[0].to_s
|
32
|
+
elsif %i[true false].include?(node.type)
|
33
|
+
node.type.to_s
|
34
|
+
elsif node.type == :nil
|
35
|
+
''
|
36
|
+
elsif node.type == :array && array_join_with
|
37
|
+
extract_array_as_string(
|
38
|
+
node,
|
39
|
+
array_join_with: array_join_with,
|
40
|
+
array_flatten: array_flatten,
|
41
|
+
array_reject_blank: array_reject_blank
|
42
|
+
).tap do |str|
|
43
|
+
# `nil` is returned when a dynamic value is encountered in strict mode. Propagate:
|
44
|
+
return nil if str.nil?
|
45
|
+
end
|
46
|
+
elsif !@scanner.config[:strict] && %i[dsym dstr].include?(node.type)
|
47
|
+
node.children.map do |child|
|
48
|
+
if %i[sym str].include?(child.type)
|
49
|
+
child.children[0].to_s
|
50
|
+
else
|
51
|
+
child.loc.expression.source
|
52
|
+
end
|
53
|
+
end.join
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Extract the whole hash from a node of type `:hash`
|
58
|
+
#
|
59
|
+
# @param node [AST::Node] a node of type `:hash`.
|
60
|
+
# @return [Hash] the whole hash from the node
|
61
|
+
def extract_hash(node)
|
62
|
+
return {} if node.nil?
|
63
|
+
|
64
|
+
if node.type == :hash
|
65
|
+
node.children.each_with_object({}) do |pair, h|
|
66
|
+
key = pair.children[0].children[0].to_s
|
67
|
+
value = pair.children[1].children[0]
|
68
|
+
h[key] = value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Extract a hash pair with a given literal key.
|
74
|
+
#
|
75
|
+
# @param node [AST::Node] a node of type `:hash`.
|
76
|
+
# @param key [String] node key as a string (indifferent symbol-string matching).
|
77
|
+
# @return [AST::Node, nil] a node of type `:pair` or nil.
|
78
|
+
def extract_hash_pair(node, key)
|
79
|
+
node.children.detect do |child|
|
80
|
+
next unless child.type == :pair
|
81
|
+
|
82
|
+
key_node = child.children[0]
|
83
|
+
%i[sym str].include?(key_node.type) && key_node.children[0].to_s == key
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Extract an array as a single string.
|
88
|
+
#
|
89
|
+
# @param array_join_with [String] joiner of the array elements.
|
90
|
+
# @param array_flatten [Boolean] if true, nested arrays are flattened,
|
91
|
+
# otherwise their source is copied and surrounded by #{}.
|
92
|
+
# @param array_reject_blank [Boolean] if true, empty strings and `nil`s are skipped.
|
93
|
+
# @return [String, nil] `nil` is returned only when a dynamic value is encountered in strict mode.
|
94
|
+
def extract_array_as_string(node, array_join_with:, array_flatten: false, array_reject_blank: false)
|
95
|
+
children_strings = node.children.map do |child|
|
96
|
+
if %i[sym str int true false].include?(child.type)
|
97
|
+
extract_string child
|
98
|
+
else
|
99
|
+
# ignore dynamic argument in strict mode
|
100
|
+
return nil if @scanner.config[:strict]
|
101
|
+
|
102
|
+
if %i[dsym dstr].include?(child.type) || (child.type == :array && array_flatten)
|
103
|
+
extract_string(child, array_join_with: array_join_with)
|
104
|
+
else
|
105
|
+
"\#{#{child.loc.expression.source}}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
if array_reject_blank
|
110
|
+
children_strings.reject! do |x|
|
111
|
+
# empty strings and nils in the scope argument are ignored by i18n
|
112
|
+
x == ''
|
113
|
+
end
|
114
|
+
end
|
115
|
+
children_strings.join(array_join_with)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks/scanners/ast_matchers/base_matcher'
|
4
|
+
require 'i18n/tasks/scanners/results/occurrence'
|
5
|
+
|
6
|
+
module I18n::Tasks::Scanners::AstMatchers
|
7
|
+
class MessageReceiversMatcher < BaseMatcher
|
8
|
+
def initialize(scanner:, receivers:, message:)
|
9
|
+
super(scanner: scanner)
|
10
|
+
@receivers = Array(receivers)
|
11
|
+
@message = message
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param send_node [Parser::AST::Node]
|
15
|
+
# @param method_name [Symbol, nil]
|
16
|
+
# @param location [Parser::Source::Map]
|
17
|
+
# @return [nil, [key, Occurrence]] full absolute key name and the occurrence.
|
18
|
+
def convert_to_key_occurrences(send_node, method_name, location: send_node.loc)
|
19
|
+
return unless node_match?(send_node)
|
20
|
+
|
21
|
+
receiver = send_node.children[0]
|
22
|
+
first_arg_node = send_node.children[2]
|
23
|
+
second_arg_node = send_node.children[3]
|
24
|
+
|
25
|
+
key = extract_string(first_arg_node)
|
26
|
+
return if key.nil?
|
27
|
+
|
28
|
+
key, default_arg = process_options(node: second_arg_node, key: key)
|
29
|
+
|
30
|
+
return if key.nil?
|
31
|
+
|
32
|
+
[
|
33
|
+
full_key(receiver: receiver, key: key, location: location, calling_method: method_name),
|
34
|
+
I18n::Tasks::Scanners::Results::Occurrence.from_range(
|
35
|
+
raw_key: key,
|
36
|
+
range: location.expression,
|
37
|
+
default_arg: default_arg
|
38
|
+
)
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def node_match?(node)
|
45
|
+
receiver = node.children[0]
|
46
|
+
message = node.children[1]
|
47
|
+
|
48
|
+
@message == message && @receivers.any? { |r| r == receiver }
|
49
|
+
end
|
50
|
+
|
51
|
+
def full_key(receiver:, key:, location:, calling_method:)
|
52
|
+
if receiver.nil?
|
53
|
+
# Relative keys only work if called via `t()` but not `I18n.t()`:
|
54
|
+
@scanner.absolute_key(
|
55
|
+
key,
|
56
|
+
location.expression.source_buffer.name,
|
57
|
+
calling_method: calling_method
|
58
|
+
)
|
59
|
+
else
|
60
|
+
key
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def process_options(node:, key:)
|
65
|
+
return [key, nil] if node&.type != :hash
|
66
|
+
|
67
|
+
scope_node = extract_hash_pair(node, 'scope')
|
68
|
+
|
69
|
+
if scope_node
|
70
|
+
scope = extract_string(
|
71
|
+
scope_node.children[1],
|
72
|
+
array_join_with: '.',
|
73
|
+
array_flatten: true,
|
74
|
+
array_reject_blank: true
|
75
|
+
)
|
76
|
+
return nil if scope.nil? && scope_node.type != :nil
|
77
|
+
|
78
|
+
key = [scope, key].join('.') unless scope == ''
|
79
|
+
end
|
80
|
+
if default_arg_node = extract_hash_pair(node, 'default')
|
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
|
86
|
+
end
|
87
|
+
|
88
|
+
[key, default_arg]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks/scanners/results/occurrence'
|
4
|
+
|
5
|
+
module I18n::Tasks::Scanners::AstMatchers
|
6
|
+
class RailsModelMatcher < BaseMatcher
|
7
|
+
def convert_to_key_occurrences(send_node, _method_name, location: send_node.loc)
|
8
|
+
human_attribute_name_to_key_occurences(send_node: send_node, location: location) ||
|
9
|
+
model_name_human_to_key_occurences(send_node: send_node, location: location)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def human_attribute_name_to_key_occurences(send_node:, location:)
|
15
|
+
children = Array(send_node&.children)
|
16
|
+
receiver = children[0]
|
17
|
+
method_name = children[1]
|
18
|
+
|
19
|
+
return unless method_name == :human_attribute_name && receiver.type == :const
|
20
|
+
|
21
|
+
value = children[2]
|
22
|
+
|
23
|
+
model_name = underscore(receiver.to_a.last)
|
24
|
+
attribute = extract_string(value)
|
25
|
+
key = "activerecord.attributes.#{model_name}.#{attribute}"
|
26
|
+
[
|
27
|
+
key,
|
28
|
+
I18n::Tasks::Scanners::Results::Occurrence.from_range(
|
29
|
+
raw_key: key,
|
30
|
+
range: location.expression
|
31
|
+
)
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
# User.model_name.human(count: 2)
|
36
|
+
# s(:send,
|
37
|
+
# s(:send,
|
38
|
+
# s(:const, nil, :User), :model_name), :human,
|
39
|
+
# s(:hash,
|
40
|
+
# s(:pair,
|
41
|
+
# s(:sym, :count),
|
42
|
+
# s(:int, 2))))
|
43
|
+
def model_name_human_to_key_occurences(send_node:, location:)
|
44
|
+
children = Array(send_node&.children)
|
45
|
+
return unless children[1] == :human
|
46
|
+
|
47
|
+
base_children = Array(children[0]&.children)
|
48
|
+
class_node = base_children[0]
|
49
|
+
|
50
|
+
return unless class_node&.type == :const && base_children[1] == :model_name
|
51
|
+
|
52
|
+
model_name = underscore(class_node.to_a.last)
|
53
|
+
key = "activerecord.models.#{model_name}"
|
54
|
+
[
|
55
|
+
key,
|
56
|
+
I18n::Tasks::Scanners::Results::Occurrence.from_range(
|
57
|
+
raw_key: key,
|
58
|
+
range: location.expression
|
59
|
+
)
|
60
|
+
]
|
61
|
+
end
|
62
|
+
|
63
|
+
def underscore(value)
|
64
|
+
value = value.dup.to_s
|
65
|
+
value.gsub!(/(.)([A-Z])/, '\1_\2')
|
66
|
+
value.downcase!
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ast'
|
4
|
+
require 'set'
|
5
|
+
require 'i18n/tasks/scanners/local_ruby_parser'
|
6
|
+
|
7
|
+
module I18n::Tasks::Scanners
|
8
|
+
class ErbAstProcessor
|
9
|
+
include AST::Processor::Mixin
|
10
|
+
def initialize
|
11
|
+
super()
|
12
|
+
@ruby_parser = LocalRubyParser.new(ignore_blocks: true)
|
13
|
+
@comments = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def process_and_extract_comments(ast)
|
17
|
+
result = process(ast)
|
18
|
+
[result, @comments]
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_code(node)
|
22
|
+
parsed, comments = @ruby_parser.parse(
|
23
|
+
node.children[0],
|
24
|
+
location: node.location
|
25
|
+
)
|
26
|
+
@comments.concat(comments)
|
27
|
+
|
28
|
+
unless parsed.nil?
|
29
|
+
parsed = parsed.updated(
|
30
|
+
nil,
|
31
|
+
parsed.children.map { |child| node?(child) ? process(child) : child }
|
32
|
+
)
|
33
|
+
node = node.updated(:send, parsed)
|
34
|
+
end
|
35
|
+
node
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param node [::Parser::AST::Node]
|
39
|
+
# @return [::Parser::AST::Node]
|
40
|
+
def handler_missing(node)
|
41
|
+
node = handle_comment(node)
|
42
|
+
return if node.nil?
|
43
|
+
|
44
|
+
node.updated(
|
45
|
+
nil,
|
46
|
+
node.children.map { |child| node?(child) ? process(child) : child }
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Convert ERB-comments to ::Parser::Source::Comment and skip processing node
|
53
|
+
#
|
54
|
+
# @param node Parser::AST::Node Potential comment node
|
55
|
+
# @return Parser::AST::Node or nil
|
56
|
+
def handle_comment(node)
|
57
|
+
if node.type == :erb && node.children.size == 4 &&
|
58
|
+
node.children[0]&.type == :indicator && node.children[0].children[0] == '#' &&
|
59
|
+
node.children[2]&.type == :code
|
60
|
+
|
61
|
+
# Do not continue parsing this node
|
62
|
+
comment = node.children[2]
|
63
|
+
@comments << ::Parser::Source::Comment.new(comment.location.expression)
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
node
|
68
|
+
end
|
69
|
+
|
70
|
+
def node?(node)
|
71
|
+
node.is_a?(::Parser::AST::Node)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/tasks/scanners/ruby_ast_scanner'
|
4
|
+
require 'i18n/tasks/scanners/erb_ast_processor'
|
5
|
+
require 'better_html/errors'
|
6
|
+
require 'better_html/parser'
|
7
|
+
|
8
|
+
module I18n::Tasks::Scanners
|
9
|
+
# Scan for I18n.translate calls in ERB-file better-html and ASTs
|
10
|
+
class ErbAstScanner < RubyAstScanner
|
11
|
+
def initialize(**args)
|
12
|
+
super(**args)
|
13
|
+
@erb_ast_processor = ErbAstProcessor.new
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Parse file on path and returns AST and comments.
|
19
|
+
#
|
20
|
+
# @param path Path to file to parse
|
21
|
+
# @return [{Parser::AST::Node}, [Parser::Source::Comment]]
|
22
|
+
def path_to_ast_and_comments(path)
|
23
|
+
parser = BetterHtml::Parser.new(make_buffer(path))
|
24
|
+
ast = convert_better_html(parser.ast)
|
25
|
+
@erb_ast_processor.process_and_extract_comments(ast)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Convert BetterHtml nodes to Parser::AST::Node
|
29
|
+
#
|
30
|
+
# @param node BetterHtml::Parser::AST::Node
|
31
|
+
# @return Parser::AST::Node
|
32
|
+
def convert_better_html(node)
|
33
|
+
definition = Parser::Source::Map::Definition.new(
|
34
|
+
node.location.begin,
|
35
|
+
node.location.begin,
|
36
|
+
node.location.begin,
|
37
|
+
node.location.end
|
38
|
+
)
|
39
|
+
Parser::AST::Node.new(
|
40
|
+
node.type,
|
41
|
+
node.children.map { |child| child.is_a?(BetterHtml::AST::Node) ? convert_better_html(child) : child },
|
42
|
+
{
|
43
|
+
location: definition
|
44
|
+
}
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'parser/current'
|
4
|
+
|
5
|
+
module I18n::Tasks::Scanners
|
6
|
+
class LocalRubyParser
|
7
|
+
# ignore_blocks feature inspired by shopify/better-html
|
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
|
10
|
+
|
11
|
+
def initialize(ignore_blocks: false)
|
12
|
+
@parser = ::Parser::CurrentRuby.new
|
13
|
+
@ignore_blocks = ignore_blocks
|
14
|
+
end
|
15
|
+
|
16
|
+
# Parse string and normalize location
|
17
|
+
def parse(source, location: nil)
|
18
|
+
buffer = ::Parser::Source::Buffer.new('(string)')
|
19
|
+
buffer.source = if @ignore_blocks
|
20
|
+
source.sub(BLOCK_EXPR, '')
|
21
|
+
else
|
22
|
+
source
|
23
|
+
end
|
24
|
+
|
25
|
+
@parser.reset
|
26
|
+
ast, comments = @parser.parse_with_comments(buffer)
|
27
|
+
ast = normalize_location(ast, location)
|
28
|
+
comments = comments.map { |comment| normalize_comment_location(comment, location) }
|
29
|
+
[ast, comments]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Normalize location for all parsed nodes
|
33
|
+
|
34
|
+
# @param node {Parser::AST::Node} Node in parsed code
|
35
|
+
# @param location {Parser::Source::Map} Global location for the parsed string
|
36
|
+
# @return {Parser::AST::Node}
|
37
|
+
def normalize_location(node, location)
|
38
|
+
return node.map { |child| normalize_location(child, location) } if node.is_a?(Array)
|
39
|
+
|
40
|
+
return node unless node.is_a?(::Parser::AST::Node)
|
41
|
+
|
42
|
+
node.updated(
|
43
|
+
nil,
|
44
|
+
node.children.map { |child| normalize_location(child, location) },
|
45
|
+
{ location: updated_location(location, node.location) }
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Calculate location relative to a global location
|
50
|
+
#
|
51
|
+
# @param global_location {Parser::Source::Map} Global location where the code was parsed
|
52
|
+
# @param local_location {Parser::Source::Map} Local location in the parsed string
|
53
|
+
# @return {Parser::Source::Map}
|
54
|
+
def updated_location(global_location, local_location)
|
55
|
+
return global_location if local_location.expression.nil?
|
56
|
+
|
57
|
+
range = ::Parser::Source::Range.new(
|
58
|
+
global_location.expression.source_buffer,
|
59
|
+
global_location.expression.to_range.begin + local_location.expression.to_range.begin,
|
60
|
+
global_location.expression.to_range.begin + local_location.expression.to_range.end
|
61
|
+
)
|
62
|
+
|
63
|
+
::Parser::Source::Map::Definition.new(
|
64
|
+
range.begin,
|
65
|
+
range.begin,
|
66
|
+
range.begin,
|
67
|
+
range.end
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Normalize location for comment
|
72
|
+
#
|
73
|
+
# @param comment {Parser::Source::Comment} A comment with local location
|
74
|
+
# @param location {Parser::Source::Map} Global location for the parsed string
|
75
|
+
# @return {Parser::Source::Comment}
|
76
|
+
def normalize_comment_location(comment, location)
|
77
|
+
range = ::Parser::Source::Range.new(
|
78
|
+
location.expression.source_buffer,
|
79
|
+
location.expression.to_range.begin + comment.location.expression.to_range.begin,
|
80
|
+
location.expression.to_range.begin + comment.location.expression.to_range.end
|
81
|
+
)
|
82
|
+
::Parser::Source::Comment.new(range)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -35,7 +35,7 @@ module I18n::Tasks::Scanners
|
|
35
35
|
result = []
|
36
36
|
text.scan(pattern) do |_|
|
37
37
|
match = Regexp.last_match
|
38
|
-
matches =
|
38
|
+
matches = match.names.map(&:to_sym).zip(match.captures).to_h
|
39
39
|
if matches.key?(:key)
|
40
40
|
matches[:key] = strip_literal(matches[:key])
|
41
41
|
next unless valid_key?(matches[:key])
|
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners
|
|
12
12
|
include OccurrenceFromPosition
|
13
13
|
include RubyKeyLiterals
|
14
14
|
|
15
|
-
TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'
|
15
|
+
TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
|
16
16
|
IGNORE_LINES = {
|
17
17
|
'coffee' => /^\s*#(?!\si18n-tasks-use)/,
|
18
18
|
'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
|
@@ -66,7 +66,7 @@ module I18n::Tasks::Scanners
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def exclude_line?(line, path)
|
69
|
-
re = @ignore_lines_res[File.extname(path)[1
|
69
|
+
re = @ignore_lines_res[File.extname(path)[1..]]
|
70
70
|
re && re =~ line
|
71
71
|
end
|
72
72
|
|
@@ -10,7 +10,7 @@ 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
|
-
exclude_method_name_paths: config[:
|
13
|
+
exclude_method_name_paths: config[:relative_exclude_method_name_paths],
|
14
14
|
calling_method: nil)
|
15
15
|
return key unless key.start_with?(DOT)
|
16
16
|
fail 'roots argument is required' unless roots.present?
|
@@ -18,7 +18,7 @@ module I18n
|
|
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
22
|
normalized_path.sub!(root, '')
|
23
23
|
|
24
24
|
if (exclude_method_name_paths || []).map { |p| expand_path(p) }.include?(root)
|
@@ -48,7 +48,7 @@ module I18n::Tasks
|
|
48
48
|
# rubocop:enable Metrics/ParameterLists
|
49
49
|
|
50
50
|
def inspect
|
51
|
-
"Occurrence(#{@path}:#{@line_num}
|
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
|
52
52
|
end
|
53
53
|
|
54
54
|
def ==(other)
|
@@ -63,6 +63,22 @@ module I18n::Tasks
|
|
63
63
|
def hash
|
64
64
|
[@path, @pos, @line_num, @line_pos, @line, @default_arg].hash
|
65
65
|
end
|
66
|
+
|
67
|
+
# @param raw_key [String]
|
68
|
+
# @param range [Parser::Source::Range]
|
69
|
+
# @param default_arg [String, nil]
|
70
|
+
# @return [Results::Occurrence]
|
71
|
+
def self.from_range(raw_key:, range:, default_arg: nil)
|
72
|
+
Occurrence.new(
|
73
|
+
path: range.source_buffer.name,
|
74
|
+
pos: range.begin_pos,
|
75
|
+
line_num: range.line,
|
76
|
+
line_pos: range.column,
|
77
|
+
line: range.source_line,
|
78
|
+
raw_key: raw_key,
|
79
|
+
default_arg: default_arg
|
80
|
+
)
|
81
|
+
end
|
66
82
|
end
|
67
83
|
end
|
68
84
|
end
|