i18n-tasks 0.9.33 → 1.0.12
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 +28 -11
- data/config/locales/en.yml +5 -3
- data/config/locales/ru.yml +1 -0
- data/i18n-tasks.gemspec +12 -6
- data/lib/i18n/tasks/base_task.rb +2 -1
- data/lib/i18n/tasks/cli.rb +27 -17
- data/lib/i18n/tasks/command/commander.rb +1 -0
- data/lib/i18n/tasks/command/commands/data.rb +8 -6
- data/lib/i18n/tasks/command/commands/eq_base.rb +2 -2
- data/lib/i18n/tasks/command/commands/health.rb +4 -3
- data/lib/i18n/tasks/command/commands/interpolations.rb +1 -1
- data/lib/i18n/tasks/command/commands/meta.rb +1 -1
- data/lib/i18n/tasks/command/commands/missing.rb +22 -9
- data/lib/i18n/tasks/command/commands/tree.rb +8 -6
- data/lib/i18n/tasks/command/commands/usages.rb +5 -4
- data/lib/i18n/tasks/command/dsl.rb +4 -4
- data/lib/i18n/tasks/command/option_parsers/enum.rb +2 -0
- data/lib/i18n/tasks/command/option_parsers/locale.rb +2 -1
- data/lib/i18n/tasks/command/options/common.rb +5 -0
- data/lib/i18n/tasks/command/options/data.rb +4 -1
- data/lib/i18n/tasks/command/options/locales.rb +5 -5
- data/lib/i18n/tasks/concurrent/cached_value.rb +2 -2
- data/lib/i18n/tasks/configuration.rb +17 -10
- data/lib/i18n/tasks/console_context.rb +1 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +9 -2
- data/lib/i18n/tasks/data/file_formats.rb +3 -1
- data/lib/i18n/tasks/data/file_system_base.rb +7 -6
- data/lib/i18n/tasks/data/router/conservative_router.rb +2 -1
- data/lib/i18n/tasks/data/router/pattern_router.rb +3 -1
- data/lib/i18n/tasks/data/tree/node.rb +6 -3
- data/lib/i18n/tasks/data/tree/nodes.rb +6 -7
- data/lib/i18n/tasks/data/tree/siblings.rb +10 -4
- data/lib/i18n/tasks/data/tree/traversal.rb +34 -11
- data/lib/i18n/tasks/html_keys.rb +4 -6
- data/lib/i18n/tasks/ignore_keys.rb +4 -3
- data/lib/i18n/tasks/interpolations.rb +10 -4
- data/lib/i18n/tasks/key_pattern_matching.rb +3 -2
- data/lib/i18n/tasks/locale_pathname.rb +1 -1
- data/lib/i18n/tasks/missing_keys.rb +4 -0
- data/lib/i18n/tasks/plural_keys.rb +5 -6
- data/lib/i18n/tasks/references.rb +4 -2
- data/lib/i18n/tasks/reports/base.rb +4 -3
- data/lib/i18n/tasks/reports/terminal.rb +8 -6
- 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/file_scanner.rb +4 -3
- data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +0 -3
- data/lib/i18n/tasks/scanners/files/file_finder.rb +3 -2
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
- data/lib/i18n/tasks/scanners/occurrence_from_position.rb +3 -3
- data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +8 -5
- data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +4 -2
- data/lib/i18n/tasks/scanners/relative_keys.rb +19 -4
- 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 -154
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +4 -4
- data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +2 -0
- data/lib/i18n/tasks/split_key.rb +3 -1
- data/lib/i18n/tasks/string_interpolation.rb +1 -0
- data/lib/i18n/tasks/translation.rb +3 -3
- data/lib/i18n/tasks/translators/base_translator.rb +5 -3
- data/lib/i18n/tasks/translators/deepl_translator.rb +10 -2
- data/lib/i18n/tasks/translators/google_translator.rb +2 -0
- data/lib/i18n/tasks/translators/yandex_translator.rb +2 -0
- data/lib/i18n/tasks/used_keys.rb +21 -14
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/i18n/tasks.rb +17 -7
- data/templates/config/i18n-tasks.yml +21 -1
- metadata +44 -15
@@ -60,11 +60,14 @@ module I18n::Tasks
|
|
60
60
|
locales.each_with_object(empty_forest) do |locale, forest|
|
61
61
|
required_keys = required_plural_keys_for_locale(locale)
|
62
62
|
next if required_keys.empty?
|
63
|
+
|
63
64
|
tree = empty_forest
|
64
65
|
plural_nodes data[locale] do |node|
|
65
66
|
children = node.children
|
66
67
|
present_keys = Set.new(children.map { |c| c.key.to_sym })
|
68
|
+
next if ignore_key?(node.full_key(root: false), :missing)
|
67
69
|
next if present_keys.superset?(required_keys)
|
70
|
+
|
68
71
|
tree[node.full_key] = node.derive(
|
69
72
|
value: children.to_hash,
|
70
73
|
children: nil,
|
@@ -132,6 +135,7 @@ module I18n::Tasks
|
|
132
135
|
locale = root.key
|
133
136
|
root.keys do |key, node|
|
134
137
|
next unless yield node
|
138
|
+
|
135
139
|
if locales_and_node_by_key.key?(key)
|
136
140
|
locales_and_node_by_key[key][0] << locale
|
137
141
|
else
|
@@ -5,12 +5,13 @@ module I18n::Tasks::PluralKeys
|
|
5
5
|
# Ref: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
6
6
|
CLDR_CATEGORY_KEYS = %w[zero one two few many other].freeze
|
7
7
|
PLURAL_KEY_SUFFIXES = Set.new CLDR_CATEGORY_KEYS
|
8
|
-
PLURAL_KEY_RE = /\.(?:#{CLDR_CATEGORY_KEYS * '|'})
|
8
|
+
PLURAL_KEY_RE = /\.(?:#{CLDR_CATEGORY_KEYS * '|'})$/.freeze
|
9
9
|
|
10
10
|
def collapse_plural_nodes!(tree)
|
11
11
|
tree.leaves.map(&:parent).compact.uniq.each do |node|
|
12
12
|
children = node.children
|
13
13
|
next unless plural_forms?(children)
|
14
|
+
|
14
15
|
node.value = children.to_hash
|
15
16
|
node.children = nil
|
16
17
|
node.data.merge! children.first.data
|
@@ -23,6 +24,7 @@ module I18n::Tasks::PluralKeys
|
|
23
24
|
# @return [String] the base form if the key is a specific plural form (e.g. apple for apple.many), the key otherwise.
|
24
25
|
def depluralize_key(key, locale = base_locale)
|
25
26
|
return key if key !~ PLURAL_KEY_RE
|
27
|
+
|
26
28
|
key_name = last_key_part(key)
|
27
29
|
parent_key = key[0..- (key_name.length + 2)]
|
28
30
|
nodes = tree("#{locale}.#{parent_key}").presence || (locale != base_locale && tree("#{base_locale}.#{parent_key}"))
|
@@ -37,10 +39,12 @@ module I18n::Tasks::PluralKeys
|
|
37
39
|
# @yieldparam node [::I18n::Tasks::Data::Tree::Node] plural node
|
38
40
|
def plural_nodes(tree)
|
39
41
|
return to_enum(:plural_nodes, tree) unless block_given?
|
42
|
+
|
40
43
|
visited = Set.new
|
41
44
|
tree.leaves do |node|
|
42
45
|
parent = node.parent
|
43
46
|
next if !parent || visited.include?(parent)
|
47
|
+
|
44
48
|
yield parent if plural_forms?(parent.children)
|
45
49
|
visited.add(parent)
|
46
50
|
end
|
@@ -48,14 +52,9 @@ module I18n::Tasks::PluralKeys
|
|
48
52
|
end
|
49
53
|
|
50
54
|
def plural_forms?(s)
|
51
|
-
return false if non_plural_other?(s)
|
52
55
|
s.present? && s.all? { |node| node.leaf? && plural_suffix?(node.key) }
|
53
56
|
end
|
54
57
|
|
55
|
-
def non_plural_other?(s)
|
56
|
-
s.size == 1 && s.first.leaf? && (!s.first.value.is_a?(String) || !s.first.value.include?('%{count}'))
|
57
|
-
end
|
58
|
-
|
59
58
|
def plural_suffix?(key)
|
60
59
|
PLURAL_KEY_SUFFIXES.include?(key)
|
61
60
|
end
|
@@ -10,12 +10,14 @@ module I18n::Tasks
|
|
10
10
|
data_refs = merge_reference_trees(data_forest.select_keys { |_, node| node.reference? }))
|
11
11
|
fail ArgumentError, 'usages must be a Data::Tree::Instance' unless usages.is_a?(Data::Tree::Siblings)
|
12
12
|
fail ArgumentError, 'all_references must be a Data::Tree::Instance' unless data_refs.is_a?(Data::Tree::Siblings)
|
13
|
+
|
13
14
|
raw_refs = empty_forest
|
14
15
|
resolved_refs = empty_forest
|
15
16
|
refs = empty_forest
|
16
17
|
data_refs.key_to_node.each do |ref_key_part, ref_node|
|
17
18
|
usages.each do |usage_node|
|
18
19
|
next unless usage_node.key == ref_key_part
|
20
|
+
|
19
21
|
if ref_node.leaf?
|
20
22
|
process_leaf!(ref_node, usage_node, raw_refs, resolved_refs, refs)
|
21
23
|
else
|
@@ -89,8 +91,8 @@ module I18n::Tasks
|
|
89
91
|
if node.value != other.value
|
90
92
|
log_warn(
|
91
93
|
'Conflicting references: '\
|
92
|
-
|
93
|
-
|
94
|
+
"#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]},"\
|
95
|
+
" but ⮕ #{other.value} in #{other.data[:locale]}"
|
94
96
|
)
|
95
97
|
end
|
96
98
|
end
|
@@ -9,6 +9,7 @@ module I18n::Tasks::Reports
|
|
9
9
|
end
|
10
10
|
|
11
11
|
attr_reader :task
|
12
|
+
|
12
13
|
delegate :base_locale, :locales, to: :task
|
13
14
|
|
14
15
|
protected
|
@@ -36,7 +37,7 @@ module I18n::Tasks::Reports
|
|
36
37
|
def used_title(keys_nodes, filter)
|
37
38
|
used_n = keys_nodes.map { |_k, node| node.data[:occurrences].size }.reduce(:+).to_i
|
38
39
|
"#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}"\
|
39
|
-
|
40
|
+
"#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
|
40
41
|
end
|
41
42
|
|
42
43
|
# Sort keys by their attributes in order
|
@@ -58,6 +59,7 @@ module I18n::Tasks::Reports
|
|
58
59
|
|
59
60
|
def format_locale(locale)
|
60
61
|
return '' unless locale
|
62
|
+
|
61
63
|
if locale.split('+') == task.locales.sort
|
62
64
|
'all'
|
63
65
|
else
|
@@ -67,8 +69,7 @@ module I18n::Tasks::Reports
|
|
67
69
|
|
68
70
|
def collapse_missing_tree!(forest)
|
69
71
|
forest = task.collapse_plural_nodes!(forest)
|
70
|
-
|
71
|
-
forest
|
72
|
+
task.collapse_same_key_in_locales!(forest) { |node| node.data[:type] == :missing_used }
|
72
73
|
end
|
73
74
|
end
|
74
75
|
end
|
@@ -106,9 +106,10 @@ module I18n
|
|
106
106
|
private
|
107
107
|
|
108
108
|
def missing_key_info(leaf)
|
109
|
-
|
109
|
+
case leaf[:type]
|
110
|
+
when :missing_used
|
110
111
|
first_occurrence leaf
|
111
|
-
|
112
|
+
when :missing_plural
|
112
113
|
leaf[:data][:missing_keys].join(', ')
|
113
114
|
else
|
114
115
|
"#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} "\
|
@@ -120,9 +121,9 @@ module I18n
|
|
120
121
|
if data[:ref_info]
|
121
122
|
from, to = data[:ref_info]
|
122
123
|
resolved = key[0...to.length]
|
123
|
-
after = key[to.length
|
124
|
+
after = key[to.length..]
|
124
125
|
" #{Rainbow(from).yellow}#{Rainbow(after).cyan}\n" \
|
125
|
-
|
126
|
+
"#{Rainbow('⮕').yellow.bright} #{Rainbow(resolved).yellow.bright}"
|
126
127
|
else
|
127
128
|
Rainbow(key).cyan
|
128
129
|
end
|
@@ -134,6 +135,7 @@ module I18n
|
|
134
135
|
|
135
136
|
def format_reference_desc(node_data)
|
136
137
|
return nil unless node_data
|
138
|
+
|
137
139
|
case node_data[:ref_type]
|
138
140
|
when :reference_usage
|
139
141
|
Rainbow('(ref)').yellow.bright
|
@@ -159,9 +161,9 @@ module I18n
|
|
159
161
|
print_table headings: [Rainbow(I18n.t('i18n_tasks.common.locale')).cyan.bright,
|
160
162
|
Rainbow(I18n.t('i18n_tasks.common.key')).cyan.bright,
|
161
163
|
I18n.t('i18n_tasks.common.value')] do |t|
|
162
|
-
t.rows = locale_key_value_datas.map
|
164
|
+
t.rows = locale_key_value_datas.map do |(locale, k, v, data)|
|
163
165
|
[{ value: Rainbow(locale).cyan, alignment: :center }, format_key(k, data), format_value(v)]
|
164
|
-
|
166
|
+
end
|
165
167
|
end
|
166
168
|
else
|
167
169
|
puts 'ø'
|
@@ -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
|
@@ -11,10 +11,11 @@ module I18n::Tasks::Scanners
|
|
11
11
|
attr_reader :config
|
12
12
|
|
13
13
|
def initialize(
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
config: {},
|
15
|
+
file_finder_provider: Files::CachingFileFinderProvider.new,
|
16
|
+
file_reader: Files::CachingFileReader.new
|
17
17
|
)
|
18
|
+
super()
|
18
19
|
@config = config
|
19
20
|
@file_reader = file_reader
|
20
21
|
@file_finder = file_finder_provider.get(**config.slice(:paths, :only, :exclude))
|
@@ -22,9 +22,6 @@ module I18n::Tasks::Scanners::Files
|
|
22
22
|
# @param (see FileFinder#traverse_files)
|
23
23
|
# @yieldparam (see FileFinder#traverse_files)
|
24
24
|
# @return (see FileFinder#traverse_files)
|
25
|
-
def traverse_files
|
26
|
-
super
|
27
|
-
end
|
28
25
|
|
29
26
|
alias uncached_find_files find_files
|
30
27
|
private :uncached_find_files
|
@@ -15,6 +15,7 @@ module I18n::Tasks::Scanners::Files
|
|
15
15
|
# Files matching any of the exclusion patterns will be excluded even if they match an inclusion pattern.
|
16
16
|
def initialize(paths: ['.'], only: nil, exclude: [])
|
17
17
|
fail 'paths argument is required' if paths.nil?
|
18
|
+
|
18
19
|
@paths = paths
|
19
20
|
@include = only
|
20
21
|
@exclude = exclude || []
|
@@ -25,8 +26,8 @@ module I18n::Tasks::Scanners::Files
|
|
25
26
|
# @yield [path]
|
26
27
|
# @yieldparam path [String] the path of the found file.
|
27
28
|
# @return [Array<of block results>]
|
28
|
-
def traverse_files
|
29
|
-
find_files.map
|
29
|
+
def traverse_files(&block)
|
30
|
+
find_files.map(&block)
|
30
31
|
end
|
31
32
|
|
32
33
|
# @return [Array<String>] found files
|