i18n-youdao-tasks 0.9.37

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +448 -0
  4. data/Rakefile +13 -0
  5. data/bin/i18n-tasks +15 -0
  6. data/bin/i18n-tasks.cmd +2 -0
  7. data/config/locales/en.yml +129 -0
  8. data/config/locales/ru.yml +131 -0
  9. data/i18n-tasks.gemspec +58 -0
  10. data/lib/i18n/tasks/base_task.rb +52 -0
  11. data/lib/i18n/tasks/cli.rb +214 -0
  12. data/lib/i18n/tasks/command/collection.rb +21 -0
  13. data/lib/i18n/tasks/command/commander.rb +38 -0
  14. data/lib/i18n/tasks/command/commands/data.rb +107 -0
  15. data/lib/i18n/tasks/command/commands/eq_base.rb +22 -0
  16. data/lib/i18n/tasks/command/commands/health.rb +30 -0
  17. data/lib/i18n/tasks/command/commands/interpolations.rb +22 -0
  18. data/lib/i18n/tasks/command/commands/meta.rb +37 -0
  19. data/lib/i18n/tasks/command/commands/missing.rb +73 -0
  20. data/lib/i18n/tasks/command/commands/tree.rb +102 -0
  21. data/lib/i18n/tasks/command/commands/usages.rb +81 -0
  22. data/lib/i18n/tasks/command/dsl.rb +56 -0
  23. data/lib/i18n/tasks/command/option_parsers/enum.rb +57 -0
  24. data/lib/i18n/tasks/command/option_parsers/locale.rb +60 -0
  25. data/lib/i18n/tasks/command/options/common.rb +47 -0
  26. data/lib/i18n/tasks/command/options/data.rb +97 -0
  27. data/lib/i18n/tasks/command/options/locales.rb +44 -0
  28. data/lib/i18n/tasks/command_error.rb +15 -0
  29. data/lib/i18n/tasks/commands.rb +29 -0
  30. data/lib/i18n/tasks/concurrent/cache.rb +22 -0
  31. data/lib/i18n/tasks/concurrent/cached_value.rb +61 -0
  32. data/lib/i18n/tasks/configuration.rb +136 -0
  33. data/lib/i18n/tasks/console_context.rb +76 -0
  34. data/lib/i18n/tasks/data/adapter/json_adapter.rb +29 -0
  35. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +27 -0
  36. data/lib/i18n/tasks/data/file_formats.rb +99 -0
  37. data/lib/i18n/tasks/data/file_system.rb +14 -0
  38. data/lib/i18n/tasks/data/file_system_base.rb +200 -0
  39. data/lib/i18n/tasks/data/router/conservative_router.rb +62 -0
  40. data/lib/i18n/tasks/data/router/pattern_router.rb +62 -0
  41. data/lib/i18n/tasks/data/tree/node.rb +206 -0
  42. data/lib/i18n/tasks/data/tree/nodes.rb +97 -0
  43. data/lib/i18n/tasks/data/tree/siblings.rb +333 -0
  44. data/lib/i18n/tasks/data/tree/traversal.rb +197 -0
  45. data/lib/i18n/tasks/data.rb +87 -0
  46. data/lib/i18n/tasks/html_keys.rb +14 -0
  47. data/lib/i18n/tasks/ignore_keys.rb +31 -0
  48. data/lib/i18n/tasks/interpolations.rb +30 -0
  49. data/lib/i18n/tasks/key_pattern_matching.rb +38 -0
  50. data/lib/i18n/tasks/locale_list.rb +19 -0
  51. data/lib/i18n/tasks/locale_pathname.rb +17 -0
  52. data/lib/i18n/tasks/logging.rb +35 -0
  53. data/lib/i18n/tasks/missing_keys.rb +185 -0
  54. data/lib/i18n/tasks/plural_keys.rb +67 -0
  55. data/lib/i18n/tasks/references.rb +103 -0
  56. data/lib/i18n/tasks/reports/base.rb +75 -0
  57. data/lib/i18n/tasks/reports/terminal.rb +243 -0
  58. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +51 -0
  59. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
  60. data/lib/i18n/tasks/scanners/file_scanner.rb +66 -0
  61. data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +35 -0
  62. data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +31 -0
  63. data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +28 -0
  64. data/lib/i18n/tasks/scanners/files/file_finder.rb +61 -0
  65. data/lib/i18n/tasks/scanners/files/file_reader.rb +19 -0
  66. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +74 -0
  67. data/lib/i18n/tasks/scanners/occurrence_from_position.rb +29 -0
  68. data/lib/i18n/tasks/scanners/pattern_mapper.rb +60 -0
  69. data/lib/i18n/tasks/scanners/pattern_scanner.rb +108 -0
  70. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +100 -0
  71. data/lib/i18n/tasks/scanners/relative_keys.rb +70 -0
  72. data/lib/i18n/tasks/scanners/results/key_occurrences.rb +54 -0
  73. data/lib/i18n/tasks/scanners/results/occurrence.rb +69 -0
  74. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +63 -0
  75. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +234 -0
  76. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +30 -0
  77. data/lib/i18n/tasks/scanners/scanner.rb +17 -0
  78. data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +43 -0
  79. data/lib/i18n/tasks/split_key.rb +72 -0
  80. data/lib/i18n/tasks/stats.rb +24 -0
  81. data/lib/i18n/tasks/string_interpolation.rb +17 -0
  82. data/lib/i18n/tasks/translation.rb +29 -0
  83. data/lib/i18n/tasks/translators/base_translator.rb +156 -0
  84. data/lib/i18n/tasks/translators/deepl_translator.rb +81 -0
  85. data/lib/i18n/tasks/translators/google_translator.rb +69 -0
  86. data/lib/i18n/tasks/translators/yandex_translator.rb +63 -0
  87. data/lib/i18n/tasks/translators/youdao_translator.rb +69 -0
  88. data/lib/i18n/tasks/unused_keys.rb +25 -0
  89. data/lib/i18n/tasks/used_keys.rb +184 -0
  90. data/lib/i18n/tasks/version.rb +7 -0
  91. data/lib/i18n/tasks.rb +69 -0
  92. data/templates/config/i18n-tasks.yml +142 -0
  93. data/templates/minitest/i18n_test.rb +36 -0
  94. data/templates/rspec/i18n_spec.rb +34 -0
  95. 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