i18n-youdao-tasks 0.9.37
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +448 -0
- data/Rakefile +13 -0
- data/bin/i18n-tasks +15 -0
- data/bin/i18n-tasks.cmd +2 -0
- data/config/locales/en.yml +129 -0
- data/config/locales/ru.yml +131 -0
- data/i18n-tasks.gemspec +58 -0
- data/lib/i18n/tasks/base_task.rb +52 -0
- data/lib/i18n/tasks/cli.rb +214 -0
- data/lib/i18n/tasks/command/collection.rb +21 -0
- data/lib/i18n/tasks/command/commander.rb +38 -0
- data/lib/i18n/tasks/command/commands/data.rb +107 -0
- data/lib/i18n/tasks/command/commands/eq_base.rb +22 -0
- data/lib/i18n/tasks/command/commands/health.rb +30 -0
- data/lib/i18n/tasks/command/commands/interpolations.rb +22 -0
- data/lib/i18n/tasks/command/commands/meta.rb +37 -0
- data/lib/i18n/tasks/command/commands/missing.rb +73 -0
- data/lib/i18n/tasks/command/commands/tree.rb +102 -0
- data/lib/i18n/tasks/command/commands/usages.rb +81 -0
- data/lib/i18n/tasks/command/dsl.rb +56 -0
- data/lib/i18n/tasks/command/option_parsers/enum.rb +57 -0
- data/lib/i18n/tasks/command/option_parsers/locale.rb +60 -0
- data/lib/i18n/tasks/command/options/common.rb +47 -0
- data/lib/i18n/tasks/command/options/data.rb +97 -0
- data/lib/i18n/tasks/command/options/locales.rb +44 -0
- data/lib/i18n/tasks/command_error.rb +15 -0
- data/lib/i18n/tasks/commands.rb +29 -0
- data/lib/i18n/tasks/concurrent/cache.rb +22 -0
- data/lib/i18n/tasks/concurrent/cached_value.rb +61 -0
- data/lib/i18n/tasks/configuration.rb +136 -0
- data/lib/i18n/tasks/console_context.rb +76 -0
- data/lib/i18n/tasks/data/adapter/json_adapter.rb +29 -0
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +27 -0
- data/lib/i18n/tasks/data/file_formats.rb +99 -0
- data/lib/i18n/tasks/data/file_system.rb +14 -0
- data/lib/i18n/tasks/data/file_system_base.rb +200 -0
- data/lib/i18n/tasks/data/router/conservative_router.rb +62 -0
- data/lib/i18n/tasks/data/router/pattern_router.rb +62 -0
- data/lib/i18n/tasks/data/tree/node.rb +206 -0
- data/lib/i18n/tasks/data/tree/nodes.rb +97 -0
- data/lib/i18n/tasks/data/tree/siblings.rb +333 -0
- data/lib/i18n/tasks/data/tree/traversal.rb +197 -0
- data/lib/i18n/tasks/data.rb +87 -0
- data/lib/i18n/tasks/html_keys.rb +14 -0
- data/lib/i18n/tasks/ignore_keys.rb +31 -0
- data/lib/i18n/tasks/interpolations.rb +30 -0
- data/lib/i18n/tasks/key_pattern_matching.rb +38 -0
- data/lib/i18n/tasks/locale_list.rb +19 -0
- data/lib/i18n/tasks/locale_pathname.rb +17 -0
- data/lib/i18n/tasks/logging.rb +35 -0
- data/lib/i18n/tasks/missing_keys.rb +185 -0
- data/lib/i18n/tasks/plural_keys.rb +67 -0
- data/lib/i18n/tasks/references.rb +103 -0
- data/lib/i18n/tasks/reports/base.rb +75 -0
- data/lib/i18n/tasks/reports/terminal.rb +243 -0
- data/lib/i18n/tasks/scanners/erb_ast_processor.rb +51 -0
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
- data/lib/i18n/tasks/scanners/file_scanner.rb +66 -0
- data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +35 -0
- data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +31 -0
- data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +28 -0
- data/lib/i18n/tasks/scanners/files/file_finder.rb +61 -0
- data/lib/i18n/tasks/scanners/files/file_reader.rb +19 -0
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +74 -0
- data/lib/i18n/tasks/scanners/occurrence_from_position.rb +29 -0
- data/lib/i18n/tasks/scanners/pattern_mapper.rb +60 -0
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +108 -0
- data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +100 -0
- data/lib/i18n/tasks/scanners/relative_keys.rb +70 -0
- data/lib/i18n/tasks/scanners/results/key_occurrences.rb +54 -0
- data/lib/i18n/tasks/scanners/results/occurrence.rb +69 -0
- data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +63 -0
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +234 -0
- data/lib/i18n/tasks/scanners/ruby_key_literals.rb +30 -0
- data/lib/i18n/tasks/scanners/scanner.rb +17 -0
- data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +43 -0
- data/lib/i18n/tasks/split_key.rb +72 -0
- data/lib/i18n/tasks/stats.rb +24 -0
- data/lib/i18n/tasks/string_interpolation.rb +17 -0
- data/lib/i18n/tasks/translation.rb +29 -0
- data/lib/i18n/tasks/translators/base_translator.rb +156 -0
- data/lib/i18n/tasks/translators/deepl_translator.rb +81 -0
- data/lib/i18n/tasks/translators/google_translator.rb +69 -0
- data/lib/i18n/tasks/translators/yandex_translator.rb +63 -0
- data/lib/i18n/tasks/translators/youdao_translator.rb +69 -0
- data/lib/i18n/tasks/unused_keys.rb +25 -0
- data/lib/i18n/tasks/used_keys.rb +184 -0
- data/lib/i18n/tasks/version.rb +7 -0
- data/lib/i18n/tasks.rb +69 -0
- data/templates/config/i18n-tasks.yml +142 -0
- data/templates/minitest/i18n_test.rb +36 -0
- data/templates/rspec/i18n_spec.rb +34 -0
- metadata +441 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module I18n::Tasks::KeyPatternMatching
|
6
|
+
extend self # rubocop:disable Style/ModuleFunction
|
7
|
+
|
8
|
+
MATCH_NOTHING = /\z\A/.freeze
|
9
|
+
|
10
|
+
# one regex to match any
|
11
|
+
def compile_patterns_re(key_patterns)
|
12
|
+
if key_patterns.blank?
|
13
|
+
# match nothing
|
14
|
+
MATCH_NOTHING
|
15
|
+
else
|
16
|
+
/(?:#{key_patterns.map { |p| compile_key_pattern p } * '|'})/m
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# convert pattern to regex
|
21
|
+
# In patterns:
|
22
|
+
# * is like .* in regexs
|
23
|
+
# : matches a single key
|
24
|
+
# { a, b.c } match any in set, can use : and *, match is captured
|
25
|
+
def compile_key_pattern(key_pattern)
|
26
|
+
return key_pattern if key_pattern.is_a?(Regexp)
|
27
|
+
|
28
|
+
/\A#{key_pattern_re_body(key_pattern)}\z/
|
29
|
+
end
|
30
|
+
|
31
|
+
def key_pattern_re_body(key_pattern)
|
32
|
+
key_pattern
|
33
|
+
.gsub(/\./, '\.')
|
34
|
+
.gsub(/\*/, '.*')
|
35
|
+
.gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)')
|
36
|
+
.gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks
|
4
|
+
module LocaleList
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# @return locales converted to strings, with base locale first, the rest sorted alphabetically
|
8
|
+
def normalize_locale_list(locales, base_locale, include_base = false)
|
9
|
+
locales = Array(locales).map(&:to_s).sort
|
10
|
+
if locales.include?(base_locale)
|
11
|
+
[base_locale] + (locales - [base_locale])
|
12
|
+
elsif include_base
|
13
|
+
[base_locale] + locales
|
14
|
+
else
|
15
|
+
locales
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks
|
4
|
+
module LocalePathname
|
5
|
+
class << self
|
6
|
+
def replace_locale(path, from, to)
|
7
|
+
path&.gsub(path_locale_re(from), to)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def path_locale_re(locale)
|
13
|
+
(@path_locale_res ||= {})[locale] ||= %r{(?<=^|[/.])#{locale}(?=[/.])}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks::Logging
|
4
|
+
module_function
|
5
|
+
|
6
|
+
MUTEX = Mutex.new
|
7
|
+
PROGRAM_NAME = File.basename($PROGRAM_NAME)
|
8
|
+
|
9
|
+
def warn_deprecated(message)
|
10
|
+
log_stderr Rainbow("#{program_name}: [DEPRECATED] #{message}").yellow.bright
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_verbose(message = nil)
|
14
|
+
log_stderr Rainbow(message || yield).blue.bright if ::I18n::Tasks.verbose?
|
15
|
+
end
|
16
|
+
|
17
|
+
def log_warn(message)
|
18
|
+
log_stderr Rainbow("#{program_name}: [WARN] #{message}").yellow
|
19
|
+
end
|
20
|
+
|
21
|
+
def log_error(message)
|
22
|
+
log_stderr Rainbow("#{program_name}: #{message}").red.bright
|
23
|
+
end
|
24
|
+
|
25
|
+
def log_stderr(*args)
|
26
|
+
# We don't want output from different threads to get intermixed.
|
27
|
+
MUTEX.synchronize do
|
28
|
+
$stderr.puts(*args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def program_name
|
33
|
+
PROGRAM_NAME
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
module I18n::Tasks
|
5
|
+
module MissingKeys # rubocop:disable Metrics/ModuleLength
|
6
|
+
MISSING_TYPES = %w[
|
7
|
+
used
|
8
|
+
diff
|
9
|
+
plural
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
def self.missing_keys_types
|
13
|
+
@missing_keys_types ||= MISSING_TYPES
|
14
|
+
end
|
15
|
+
|
16
|
+
def missing_keys_types
|
17
|
+
MissingKeys.missing_keys_types
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param types [:used, :diff, :plural] all if `nil`.
|
21
|
+
# @return [Siblings]
|
22
|
+
def missing_keys(locales: nil, types: nil, base_locale: nil)
|
23
|
+
locales ||= self.locales
|
24
|
+
types ||= missing_keys_types
|
25
|
+
base = base_locale || self.base_locale
|
26
|
+
types.inject(empty_forest) do |f, type|
|
27
|
+
f.merge! send(:"missing_#{type}_forest", locales, base)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def eq_base_keys(opts = {})
|
32
|
+
locales = Array(opts[:locales]).presence || self.locales
|
33
|
+
(locales - [base_locale]).inject(empty_forest) do |tree, locale|
|
34
|
+
tree.merge! equal_values_tree(locale, base_locale)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def missing_diff_forest(locales, base = base_locale)
|
39
|
+
tree = empty_forest
|
40
|
+
# present in base but not locale
|
41
|
+
(locales - [base]).each do |locale|
|
42
|
+
tree.merge! missing_diff_tree(locale, base)
|
43
|
+
end
|
44
|
+
if locales.include?(base)
|
45
|
+
# present in locale but not base
|
46
|
+
(self.locales - [base]).each do |locale|
|
47
|
+
tree.merge! missing_diff_tree(base, locale)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
tree
|
51
|
+
end
|
52
|
+
|
53
|
+
def missing_used_forest(locales, _base = base_locale)
|
54
|
+
locales.inject(empty_forest) do |forest, locale|
|
55
|
+
forest.merge! missing_used_tree(locale)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def missing_plural_forest(locales, _base = base_locale)
|
60
|
+
locales.each_with_object(empty_forest) do |locale, forest|
|
61
|
+
required_keys = required_plural_keys_for_locale(locale)
|
62
|
+
next if required_keys.empty?
|
63
|
+
|
64
|
+
tree = empty_forest
|
65
|
+
plural_nodes data[locale] do |node|
|
66
|
+
children = node.children
|
67
|
+
present_keys = Set.new(children.map { |c| c.key.to_sym })
|
68
|
+
next if ignore_key?(node.full_key(root: false), :missing)
|
69
|
+
next if present_keys.superset?(required_keys)
|
70
|
+
|
71
|
+
tree[node.full_key] = node.derive(
|
72
|
+
value: children.to_hash,
|
73
|
+
children: nil,
|
74
|
+
data: node.data.merge(missing_keys: (required_keys - present_keys).to_a)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
tree.set_root_key!(locale, type: :missing_plural)
|
78
|
+
forest.merge!(tree)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def required_plural_keys_for_locale(locale)
|
83
|
+
@plural_keys_for_locale ||= {}
|
84
|
+
return @plural_keys_for_locale[locale] if @plural_keys_for_locale.key?(locale)
|
85
|
+
|
86
|
+
@plural_keys_for_locale[locale] = plural_keys_for_locale(locale)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Loads rails-i18n pluralization config for the given locale.
|
90
|
+
def load_rails_i18n_pluralization!(locale)
|
91
|
+
path = File.join(Gem::Specification.find_by_name('rails-i18n').gem_dir, 'rails', 'pluralization', "#{locale}.rb")
|
92
|
+
eval(File.read(path), binding, path) # rubocop:disable Security/Eval
|
93
|
+
end
|
94
|
+
|
95
|
+
# keys present in compared_to, but not in locale
|
96
|
+
def missing_diff_tree(locale, compared_to = base_locale)
|
97
|
+
data[compared_to].select_keys do |key, _node|
|
98
|
+
locale_key_missing? locale, depluralize_key(key, compared_to)
|
99
|
+
end.set_root_key!(locale, type: :missing_diff).keys do |_key, node|
|
100
|
+
# change path and locale to base
|
101
|
+
data = { locale: locale, missing_diff_locale: node.data[:locale] }
|
102
|
+
if node.data.key?(:path)
|
103
|
+
data[:path] = LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale)
|
104
|
+
end
|
105
|
+
node.data.update data
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# keys used in the code missing translations in locale
|
110
|
+
def missing_used_tree(locale)
|
111
|
+
used_tree(strict: true).select_keys do |key, _node|
|
112
|
+
locale_key_missing?(locale, key)
|
113
|
+
end.set_root_key!(locale, type: :missing_used)
|
114
|
+
end
|
115
|
+
|
116
|
+
def equal_values_tree(locale, compare_to = base_locale)
|
117
|
+
base = data[compare_to].first.children
|
118
|
+
data[locale].select_keys(root: false) do |key, node|
|
119
|
+
other_node = base[key]
|
120
|
+
other_node && !node.reference? && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
|
121
|
+
end.set_root_key!(locale, type: :eq_base)
|
122
|
+
end
|
123
|
+
|
124
|
+
def locale_key_missing?(locale, key)
|
125
|
+
!key_value?(key, locale) && !external_key?(key, locale) && !ignore_key?(key, :missing)
|
126
|
+
end
|
127
|
+
|
128
|
+
# @param [::I18n::Tasks::Data::Tree::Siblings] forest
|
129
|
+
# @yield [::I18n::Tasks::Data::Tree::Node]
|
130
|
+
# @yieldreturn [Boolean] whether to collapse the node
|
131
|
+
def collapse_same_key_in_locales!(forest)
|
132
|
+
locales_and_node_by_key = {}
|
133
|
+
to_remove = []
|
134
|
+
forest.each do |root|
|
135
|
+
locale = root.key
|
136
|
+
root.keys do |key, node|
|
137
|
+
next unless yield node
|
138
|
+
|
139
|
+
if locales_and_node_by_key.key?(key)
|
140
|
+
locales_and_node_by_key[key][0] << locale
|
141
|
+
else
|
142
|
+
locales_and_node_by_key[key] = [[locale], node]
|
143
|
+
end
|
144
|
+
to_remove << node
|
145
|
+
end
|
146
|
+
end
|
147
|
+
forest.remove_nodes_and_emptied_ancestors! to_remove
|
148
|
+
locales_and_node_by_key.each_with_object({}) do |(key, (locales, node)), inv|
|
149
|
+
(inv[locales.sort.join('+')] ||= []) << [key, node]
|
150
|
+
end.map do |locales, keys_nodes|
|
151
|
+
keys_nodes.each do |(key, node)|
|
152
|
+
forest["#{locales}.#{key}"] = node
|
153
|
+
end
|
154
|
+
end
|
155
|
+
forest
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def plural_keys_for_locale(locale)
|
161
|
+
configuration = load_rails_i18n_pluralization!(locale)
|
162
|
+
if configuration[locale.to_sym].nil?
|
163
|
+
alternate_locale = alternate_locale_from(locale)
|
164
|
+
return Set.new if configuration[alternate_locale.to_sym].nil?
|
165
|
+
|
166
|
+
return set_from_rails_i18n_pluralization(configuration, alternate_locale)
|
167
|
+
end
|
168
|
+
set_from_rails_i18n_pluralization(configuration, locale)
|
169
|
+
rescue SystemCallError, IOError
|
170
|
+
Set.new
|
171
|
+
end
|
172
|
+
|
173
|
+
def alternate_locale_from(locale)
|
174
|
+
re = /(\w{2})-*(\w{2,3})*/
|
175
|
+
match = locale.match(re)
|
176
|
+
language_code = match[1]
|
177
|
+
country_code = match[2]
|
178
|
+
"#{language_code}-#{country_code.upcase}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_from_rails_i18n_pluralization(configuration, locale)
|
182
|
+
Set.new(configuration[locale.to_sym][:i18n][:plural][:keys])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
module I18n::Tasks::PluralKeys
|
5
|
+
# Ref: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
6
|
+
CLDR_CATEGORY_KEYS = %w[zero one two few many other].freeze
|
7
|
+
PLURAL_KEY_SUFFIXES = Set.new CLDR_CATEGORY_KEYS
|
8
|
+
PLURAL_KEY_RE = /\.(?:#{CLDR_CATEGORY_KEYS * '|'})$/.freeze
|
9
|
+
|
10
|
+
def collapse_plural_nodes!(tree)
|
11
|
+
tree.leaves.map(&:parent).compact.uniq.each do |node|
|
12
|
+
children = node.children
|
13
|
+
next unless plural_forms?(children)
|
14
|
+
|
15
|
+
node.value = children.to_hash
|
16
|
+
node.children = nil
|
17
|
+
node.data.merge! children.first.data
|
18
|
+
end
|
19
|
+
tree
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [String] key i18n key
|
23
|
+
# @param [String] locale to pull key data from
|
24
|
+
# @return [String] the base form if the key is a specific plural form (e.g. apple for apple.many), the key otherwise.
|
25
|
+
def depluralize_key(key, locale = base_locale)
|
26
|
+
return key if key !~ PLURAL_KEY_RE
|
27
|
+
|
28
|
+
key_name = last_key_part(key)
|
29
|
+
parent_key = key[0..- (key_name.length + 2)]
|
30
|
+
nodes = tree("#{locale}.#{parent_key}").presence || (locale != base_locale && tree("#{base_locale}.#{parent_key}"))
|
31
|
+
if nodes && plural_forms?(nodes)
|
32
|
+
parent_key
|
33
|
+
else
|
34
|
+
key
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [::I18n::Tasks::Data::Tree::Traversal] tree
|
39
|
+
# @yieldparam node [::I18n::Tasks::Data::Tree::Node] plural node
|
40
|
+
def plural_nodes(tree)
|
41
|
+
return to_enum(:plural_nodes, tree) unless block_given?
|
42
|
+
|
43
|
+
visited = Set.new
|
44
|
+
tree.leaves do |node|
|
45
|
+
parent = node.parent
|
46
|
+
next if !parent || visited.include?(parent)
|
47
|
+
|
48
|
+
yield parent if plural_forms?(parent.children)
|
49
|
+
visited.add(parent)
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def plural_forms?(s)
|
55
|
+
return false if non_plural_other?(s)
|
56
|
+
|
57
|
+
s.present? && s.all? { |node| node.leaf? && plural_suffix?(node.key) }
|
58
|
+
end
|
59
|
+
|
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
|
+
def plural_suffix?(key)
|
65
|
+
PLURAL_KEY_SUFFIXES.include?(key)
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks
|
4
|
+
module References
|
5
|
+
# Given a raw usage tree and a tree of reference keys in the data, return 3 trees:
|
6
|
+
# 1. Raw references -- a subset of the usages tree with keys that are reference key usages.
|
7
|
+
# 2. Resolved references -- all the used references in their fully resolved form.
|
8
|
+
# 3. Reference keys -- all the used reference keys.
|
9
|
+
def process_references(usages,
|
10
|
+
data_refs = merge_reference_trees(data_forest.select_keys { |_, node| node.reference? }))
|
11
|
+
fail ArgumentError, 'usages must be a Data::Tree::Instance' unless usages.is_a?(Data::Tree::Siblings)
|
12
|
+
fail ArgumentError, 'all_references must be a Data::Tree::Instance' unless data_refs.is_a?(Data::Tree::Siblings)
|
13
|
+
|
14
|
+
raw_refs = empty_forest
|
15
|
+
resolved_refs = empty_forest
|
16
|
+
refs = empty_forest
|
17
|
+
data_refs.key_to_node.each do |ref_key_part, ref_node|
|
18
|
+
usages.each do |usage_node|
|
19
|
+
next unless usage_node.key == ref_key_part
|
20
|
+
|
21
|
+
if ref_node.leaf?
|
22
|
+
process_leaf!(ref_node, usage_node, raw_refs, resolved_refs, refs)
|
23
|
+
else
|
24
|
+
process_non_leaf!(ref_node, usage_node, raw_refs, resolved_refs, refs)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
[raw_refs, resolved_refs, refs]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @param [I18n::Tasks::Data::Tree::Node] ref
|
34
|
+
# @param [I18n::Tasks::Data::Tree::Node] usage
|
35
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] raw_refs
|
36
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] resolved_refs
|
37
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] refs
|
38
|
+
def process_leaf!(ref, usage, raw_refs, resolved_refs, refs)
|
39
|
+
refs.merge_node!(Data::Tree::Node.new(key: ref.key, data: usage.data)) unless refs.key_to_node.key?(ref.key)
|
40
|
+
new_resolved_refs = Data::Tree::Siblings.from_key_names([ref.value.to_s]) do |_, resolved_node|
|
41
|
+
raw_refs.merge_node!(usage)
|
42
|
+
if usage.leaf?
|
43
|
+
resolved_node.data.merge!(usage.data)
|
44
|
+
else
|
45
|
+
resolved_node.children = usage.children
|
46
|
+
end
|
47
|
+
resolved_node.leaves { |node| node.data[:ref_info] = [ref.full_key, ref.value.to_s] }
|
48
|
+
end
|
49
|
+
add_occurences! refs.key_to_node[ref.key].data, new_resolved_refs
|
50
|
+
resolved_refs.merge! new_resolved_refs
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Hash] ref_data
|
54
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] new_resolved_refs
|
55
|
+
def add_occurences!(ref_data, new_resolved_refs)
|
56
|
+
ref_data[:occurrences] ||= []
|
57
|
+
new_resolved_refs.leaves do |leaf|
|
58
|
+
ref_data[:occurrences].concat(leaf.data[:occurrences] || [])
|
59
|
+
end
|
60
|
+
ref_data[:occurrences].sort_by!(&:path)
|
61
|
+
ref_data[:occurrences].uniq!
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param [I18n::Tasks::Data::Tree::Node] ref
|
65
|
+
# @param [I18n::Tasks::Data::Tree::Node] usage
|
66
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] raw_refs
|
67
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] resolved_refs
|
68
|
+
# @param [I18n::Tasks::Data::Tree::Siblings] refs
|
69
|
+
def process_non_leaf!(ref, usage, raw_refs, resolved_refs, refs)
|
70
|
+
child_raw_refs, child_resolved_refs, child_refs = process_references(usage.children, ref.children)
|
71
|
+
raw_refs.merge_node! Data::Tree::Node.new(key: ref.key, children: child_raw_refs) unless child_raw_refs.empty?
|
72
|
+
resolved_refs.merge! child_resolved_refs
|
73
|
+
refs.merge_node! Data::Tree::Node.new(key: ref.key, children: child_refs) unless child_refs.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Given a forest of references, merge trees into one tree, ensuring there are no conflicting references.
|
77
|
+
# @param roots [I18n::Tasks::Data::Tree::Siblings]
|
78
|
+
# @return [I18n::Tasks::Data::Tree::Siblings]
|
79
|
+
def merge_reference_trees(roots)
|
80
|
+
roots.inject(empty_forest) do |forest, root|
|
81
|
+
root.keys do |full_key, node|
|
82
|
+
if full_key == node.value.to_s
|
83
|
+
log_warn(
|
84
|
+
"Self-referencing key #{node.full_key(root: false).inspect} in #{node.data[:locale].inspect}"
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
forest.merge!(
|
89
|
+
root.children,
|
90
|
+
on_leaves_merge: lambda do |node, other|
|
91
|
+
if node.value != other.value
|
92
|
+
log_warn(
|
93
|
+
'Conflicting references: '\
|
94
|
+
"#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]},"\
|
95
|
+
" but ⮕ #{other.value} in #{other.data[:locale]}"
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n::Tasks::Reports
|
4
|
+
class Base
|
5
|
+
include I18n::Tasks::Logging
|
6
|
+
|
7
|
+
def initialize(task = I18n::Tasks::BaseTask.new)
|
8
|
+
@task = task
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :task
|
12
|
+
|
13
|
+
delegate :base_locale, :locales, to: :task
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def missing_type_info(type)
|
18
|
+
::I18n::Tasks::MissingKeys::MISSING_TYPES[type.to_s.sub(/\Amissing_/, '').to_sym]
|
19
|
+
end
|
20
|
+
|
21
|
+
def missing_title(forest)
|
22
|
+
"Missing translations (#{forest.leaves.count || '∅'})"
|
23
|
+
end
|
24
|
+
|
25
|
+
def inconsistent_interpolations_title(forest)
|
26
|
+
"Inconsistent interpolations (#{forest.leaves.count || '∅'})"
|
27
|
+
end
|
28
|
+
|
29
|
+
def unused_title(key_values)
|
30
|
+
"Unused keys (#{key_values.count || '∅'})"
|
31
|
+
end
|
32
|
+
|
33
|
+
def eq_base_title(key_values, locale = base_locale)
|
34
|
+
"Same value as #{locale} (#{key_values.count || '∅'})"
|
35
|
+
end
|
36
|
+
|
37
|
+
def used_title(keys_nodes, filter)
|
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
|
+
"#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sort keys by their attributes in order
|
44
|
+
# @param [Hash] order e.g. {locale: :asc, type: :desc, key: :asc}
|
45
|
+
def sort_by_attr!(objects, order = { locale: :asc, key: :asc })
|
46
|
+
order_keys = order.keys
|
47
|
+
objects.sort! do |a, b|
|
48
|
+
by = order_keys.detect { |k| a[k] != b[k] }
|
49
|
+
order[by] == :desc ? b[by] <=> a[by] : a[by] <=> b[by]
|
50
|
+
end
|
51
|
+
objects
|
52
|
+
end
|
53
|
+
|
54
|
+
def forest_to_attr(forest)
|
55
|
+
forest.keys(root: false).map do |key, node|
|
56
|
+
{ key: key, value: node.value, type: node.data[:type], locale: node.root.key, data: node.data }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def format_locale(locale)
|
61
|
+
return '' unless locale
|
62
|
+
|
63
|
+
if locale.split('+') == task.locales.sort
|
64
|
+
'all'
|
65
|
+
else
|
66
|
+
locale.tr '+', ' '
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def collapse_missing_tree!(forest)
|
71
|
+
forest = task.collapse_plural_nodes!(forest)
|
72
|
+
task.collapse_same_key_in_locales!(forest) { |node| node.data[:type] == :missing_used }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|