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,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'i18n/tasks/split_key'
5
+ require 'i18n/tasks/data/tree/nodes'
6
+ require 'i18n/tasks/data/tree/node'
7
+
8
+ module I18n::Tasks::Data::Tree
9
+ # Siblings represents a subtree sharing a common parent
10
+ # in case of an empty parent (nil) it represents a forest
11
+ # siblings' keys are unique
12
+ class Siblings < Nodes # rubocop:disable Metrics/ClassLength
13
+ include ::I18n::Tasks::SplitKey
14
+ include ::I18n::Tasks::PluralKeys
15
+
16
+ attr_reader :parent, :key_to_node
17
+
18
+ def initialize(opts = {})
19
+ super(nodes: opts[:nodes])
20
+ @parent = opts[:parent] || first.try(:parent)
21
+ @list.map! { |node| node.parent == @parent ? node : node.derive(parent: @parent) }
22
+ @key_to_node = @list.each_with_object({}) { |node, h| h[node.key] = node }
23
+ @warn_about_add_children_to_leaf = opts.fetch(:warn_about_add_children_to_leaf, true)
24
+ end
25
+
26
+ def attributes
27
+ super.merge(parent: @parent)
28
+ end
29
+
30
+ def rename_key(key, new_key)
31
+ node = key_to_node.delete(key)
32
+ replace_node! node, node.derive(key: new_key)
33
+ self
34
+ end
35
+
36
+ # @param from_pattern [Regexp]
37
+ # @param to_pattern [Regexp]
38
+ # @param root [Boolean]
39
+ # @return {old key => new key}
40
+ def mv_key!(from_pattern, to_pattern, root: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
41
+ moved_forest = Siblings.new
42
+ moved_nodes = []
43
+ old_key_to_new_key = {}
44
+ nodes do |node|
45
+ full_key = node.full_key(root: root)
46
+ if from_pattern =~ full_key
47
+ moved_nodes << node
48
+ if to_pattern.empty?
49
+ old_key_to_new_key[full_key] = nil
50
+ next
51
+ end
52
+ match = $~
53
+ new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..-1].to_i] }
54
+ old_key_to_new_key[full_key] = new_key
55
+ moved_forest.merge!(Siblings.new.tap do |forest|
56
+ forest[[(node.root.try(:key) unless root), new_key].compact.join('.')] =
57
+ node.derive(key: split_key(new_key).last)
58
+ end)
59
+ end
60
+ end
61
+ # Adjust references
62
+ # TODO: support nested references better
63
+ nodes do |node|
64
+ next unless node.reference?
65
+
66
+ old_target = [(node.root.key if root), node.value.to_s].compact.join('.')
67
+ new_target = old_key_to_new_key[old_target]
68
+ if new_target
69
+ new_target = new_target.sub(/\A[^.]*\./, '') if root
70
+ node.value = new_target.to_sym
71
+ end
72
+ end
73
+ remove_nodes_and_emptied_ancestors! moved_nodes
74
+ merge! moved_forest
75
+ old_key_to_new_key
76
+ end
77
+
78
+ def replace_node!(node, new_node)
79
+ @list[@list.index(node)] = new_node
80
+ key_to_node[new_node.key] = new_node
81
+ end
82
+
83
+ # @return [Node] by full key
84
+ def get(full_key)
85
+ first_key, rest = split_key(full_key.to_s, 2)
86
+ node = key_to_node[first_key]
87
+ node = node.children.try(:get, rest) if rest && node
88
+ node
89
+ end
90
+
91
+ alias [] get
92
+
93
+ # add or replace node by full key
94
+ def set(full_key, node)
95
+ fail 'value should be a I18n::Tasks::Data::Tree::Node' unless node.is_a?(Node)
96
+
97
+ key_part, rest = split_key(full_key, 2)
98
+ child = key_to_node[key_part]
99
+
100
+ if rest
101
+ unless child
102
+ child = Node.new(
103
+ key: key_part,
104
+ parent: parent,
105
+ children: [],
106
+ warn_about_add_children_to_leaf: @warn_about_add_children_to_leaf
107
+ )
108
+ append! child
109
+ end
110
+ unless child.children
111
+ conditionally_warn_add_children_to_leaf(child, [])
112
+ child.children = []
113
+ end
114
+ child.children.set rest, node
115
+ else
116
+ remove! child if child
117
+ append! node
118
+ end
119
+ dirty!
120
+ node
121
+ end
122
+
123
+ alias []= set
124
+
125
+ # methods below change state
126
+
127
+ def remove!(node)
128
+ super
129
+ key_to_node.delete(node.key)
130
+ self
131
+ end
132
+
133
+ def append!(nodes)
134
+ nodes = nodes.map do |node|
135
+ fail "already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
136
+
137
+ key_to_node[node.key] = (node.parent == parent ? node : node.derive(parent: parent))
138
+ end
139
+ super(nodes)
140
+ self
141
+ end
142
+
143
+ def append(nodes)
144
+ derive.append!(nodes)
145
+ end
146
+
147
+ # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
148
+ def merge!(nodes, on_leaves_merge: nil)
149
+ nodes = Siblings.from_nested_hash(nodes) if nodes.is_a?(Hash)
150
+ nodes.each do |node|
151
+ merge_node! node, on_leaves_merge: on_leaves_merge
152
+ end
153
+ self
154
+ end
155
+
156
+ def merge(nodes)
157
+ derive.merge!(nodes)
158
+ end
159
+
160
+ def subtract_keys(keys)
161
+ remove_nodes_and_emptied_ancestors(find_nodes(keys))
162
+ end
163
+
164
+ def subtract_keys!(keys)
165
+ remove_nodes_and_emptied_ancestors!(find_nodes(keys))
166
+ end
167
+
168
+ def subtract_by_key(other)
169
+ subtract_keys other.key_names(root: true)
170
+ end
171
+
172
+ def subtract_by_key!(other)
173
+ subtract_keys! other.key_names(root: true)
174
+ end
175
+
176
+ def set_root_key!(new_key, data = nil)
177
+ return self if empty?
178
+
179
+ rename_key first.key, new_key
180
+ leaves { |node| node.data.merge! data } if data
181
+ self
182
+ end
183
+
184
+ # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
185
+ def merge_node!(node, on_leaves_merge: nil) # rubocop:disable Metrics/AbcSize
186
+ if key_to_node.key?(node.key)
187
+ our = key_to_node[node.key]
188
+ return if our == node
189
+
190
+ our.value = node.value if node.leaf?
191
+ our.data.merge!(node.data) if node.data?
192
+ if node.children?
193
+ if our.children
194
+ our.children.merge!(node.children)
195
+ else
196
+ conditionally_warn_add_children_to_leaf(our, node.children)
197
+ our.children = node.children
198
+ end
199
+ elsif on_leaves_merge
200
+ on_leaves_merge.call(our, node)
201
+ end
202
+ else
203
+ @list << (key_to_node[node.key] = node.derive(parent: parent))
204
+ dirty!
205
+ end
206
+ end
207
+
208
+ # @param nodes [Enumerable] Modified in-place.
209
+ def remove_nodes_and_emptied_ancestors(nodes)
210
+ add_ancestors_that_only_contain_nodes! nodes
211
+ select_nodes { |node| !nodes.include?(node) }
212
+ end
213
+
214
+ # @param nodes [Enumerable] Modified in-place.
215
+ def remove_nodes_and_emptied_ancestors!(nodes)
216
+ add_ancestors_that_only_contain_nodes! nodes
217
+ select_nodes! { |node| !nodes.include?(node) }
218
+ end
219
+
220
+ private
221
+
222
+ def find_nodes(keys)
223
+ keys.each_with_object(Set.new) do |key, set|
224
+ node = get(key)
225
+ set << node if node
226
+ end
227
+ end
228
+
229
+ # Adds all the ancestors that only contain the given nodes as descendants to the given nodes.
230
+ # @param nodes [Set] Modified in-place.
231
+ def add_ancestors_that_only_contain_nodes!(nodes)
232
+ levels.reverse_each do |level_nodes|
233
+ level_nodes.each { |node| nodes << node if node.children? && node.children.all? { |c| nodes.include?(c) } }
234
+ end
235
+ end
236
+
237
+ def conditionally_warn_add_children_to_leaf(node, children)
238
+ return unless @warn_about_add_children_to_leaf
239
+ return if plural_forms?(children)
240
+
241
+ warn_add_children_to_leaf(node)
242
+ end
243
+
244
+ def warn_add_children_to_leaf(node)
245
+ ::I18n::Tasks::Logging.log_warn "'#{node.full_key}' was a leaf, now has children (value <- scope conflict)"
246
+ end
247
+
248
+ class << self
249
+ include ::I18n::Tasks::SplitKey
250
+
251
+ def null
252
+ new
253
+ end
254
+
255
+ def build_forest(opts = {}, &block)
256
+ opts[:nodes] ||= []
257
+ parse_parent_opt!(opts)
258
+ forest = Siblings.new(opts)
259
+ yield(forest) if block
260
+ # forest.parent.children = forest
261
+ forest
262
+ end
263
+
264
+ # @param key_occurrences [I18n::Tasks::Scanners::KeyOccurrences]
265
+ # @return [Siblings]
266
+ def from_key_occurrences(key_occurrences)
267
+ build_forest(warn_about_add_children_to_leaf: false) do |forest|
268
+ key_occurrences.each do |key_occurrence|
269
+ forest[key_occurrence.key] = ::I18n::Tasks::Data::Tree::Node.new(
270
+ key: split_key(key_occurrence.key).last,
271
+ data: { occurrences: key_occurrence.occurrences }
272
+ )
273
+ end
274
+ end
275
+ end
276
+
277
+ def from_key_attr(key_attrs, opts = {}, &block)
278
+ build_forest(opts) do |forest|
279
+ key_attrs.each do |(full_key, attr)|
280
+ fail "Invalid key #{full_key.inspect}" if full_key.end_with?('.')
281
+
282
+ node = ::I18n::Tasks::Data::Tree::Node.new(**attr.merge(key: split_key(full_key).last))
283
+ yield(full_key, node) if block
284
+ forest[full_key] = node
285
+ end
286
+ end
287
+ end
288
+
289
+ def from_key_names(keys, opts = {}, &block)
290
+ build_forest(opts) do |forest|
291
+ keys.each do |full_key|
292
+ node = ::I18n::Tasks::Data::Tree::Node.new(key: split_key(full_key).last)
293
+ yield(full_key, node) if block
294
+ forest[full_key] = node
295
+ end
296
+ end
297
+ end
298
+
299
+ # build forest from nested hash, e.g. {'es' => { 'common' => { name => 'Nombre', 'age' => 'Edad' } } }
300
+ # this is the native i18n gem format
301
+ def from_nested_hash(hash, opts = {})
302
+ parse_parent_opt!(opts)
303
+ fail I18n::Tasks::CommandError, "invalid tree #{hash.inspect}" unless hash.respond_to?(:map)
304
+
305
+ opts[:nodes] = hash.map { |key, value| Node.from_key_value key, value }
306
+ Siblings.new(opts)
307
+ end
308
+
309
+ alias [] from_nested_hash
310
+
311
+ # build forest from [[Full Key, Value]]
312
+ def from_flat_pairs(pairs)
313
+ Siblings.new.tap do |siblings|
314
+ pairs.each do |full_key, value|
315
+ siblings[full_key] = ::I18n::Tasks::Data::Tree::Node.new(key: split_key(full_key).last, value: value)
316
+ end
317
+ end
318
+ end
319
+
320
+ private
321
+
322
+ def parse_parent_opt!(opts)
323
+ opts[:parent] = ::I18n::Tasks::Data::Tree::Node.new(key: opts[:parent_key]) if opts[:parent_key]
324
+ opts[:parent] = ::I18n::Tasks::Data::Tree::Node.new(opts[:parent_attr]) if opts[:parent_attr]
325
+ if opts[:parent_locale]
326
+ opts[:parent] = ::I18n::Tasks::Data::Tree::Node.new(
327
+ key: opts[:parent_locale], data: { locale: opts[:parent_locale] }
328
+ )
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module I18n::Tasks
6
+ module Data::Tree
7
+ # Any Enumerable that yields nodes can mix in this module
8
+ module Traversal # rubocop:disable Metrics/ModuleLength
9
+ def nodes(&block)
10
+ depth_first(&block)
11
+ end
12
+
13
+ def leaves(&visitor)
14
+ return to_enum(:leaves) unless visitor
15
+
16
+ nodes do |node|
17
+ visitor.yield(node) if node.leaf?
18
+ end
19
+ self
20
+ end
21
+
22
+ def levels(&block)
23
+ return to_enum(:levels) unless block
24
+
25
+ nodes = to_nodes
26
+ unless nodes.empty?
27
+ block.yield nodes
28
+ if nodes.size == 1
29
+ node = first
30
+ node.children.levels(&block) if node.children?
31
+ else
32
+ Nodes.new(nodes: nodes.children).levels(&block)
33
+ end
34
+ end
35
+ self
36
+ end
37
+
38
+ def breadth_first(&visitor)
39
+ return to_enum(:breadth_first) unless visitor
40
+
41
+ levels do |nodes|
42
+ nodes.each { |node| visitor.yield(node) }
43
+ end
44
+ self
45
+ end
46
+
47
+ def depth_first(&visitor)
48
+ return to_enum(:depth_first) unless visitor
49
+
50
+ each do |node|
51
+ visitor.yield node
52
+ next unless node.children?
53
+
54
+ node.children.each do |child|
55
+ child.depth_first(&visitor)
56
+ end
57
+ end
58
+ self
59
+ end
60
+
61
+ # @option root include root in full key
62
+ def keys(root: false, &visitor)
63
+ return to_enum(:keys, root: root) unless visitor
64
+
65
+ leaves { |node| visitor.yield(node.full_key(root: root), node) }
66
+ self
67
+ end
68
+
69
+ def key_names(root: false)
70
+ keys(root: root).map { |key, _node| key }
71
+ end
72
+
73
+ def key_values(root: false)
74
+ keys(root: root).map { |key, node| [key, node.value] }
75
+ end
76
+
77
+ def root_key_values(sort = false)
78
+ result = keys(root: false).map { |key, node| [node.root.key, key, node.value] }
79
+ result.sort! { |a, b| a[0] == b[0] ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
80
+ result
81
+ end
82
+
83
+ def root_key_value_data(sort = false)
84
+ result = keys(root: false).map { |key, node| [node.root.key, key, node.value, node.data] }
85
+ result.sort! { |a, b| a[0] == b[0] ? a[1] <=> b[1] : a[0] <=> b[0] } if sort
86
+ result
87
+ end
88
+
89
+ #-- modify / derive
90
+
91
+ # Select the nodes for which the block returns true. Pre-order traversal.
92
+ # @return [Siblings] a new tree
93
+ def select_nodes(&block)
94
+ tree = Siblings.new
95
+ each do |node|
96
+ next unless block.yield(node)
97
+
98
+ tree.append! node.derive(
99
+ parent: tree.parent,
100
+ children: (node.children.select_nodes(&block).to_a if node.children)
101
+ )
102
+ end
103
+ tree
104
+ end
105
+
106
+ # Select the nodes for which the block returns true. Pre-order traversal.
107
+ # @return [Siblings] self
108
+ def select_nodes!(&block)
109
+ to_remove = []
110
+ each do |node|
111
+ if block.yield(node)
112
+ node.children&.select_nodes!(&block)
113
+ else
114
+ # removing during each is unsafe
115
+ to_remove << node
116
+ end
117
+ end
118
+ to_remove.each { |node| remove! node }
119
+ self
120
+ end
121
+
122
+ # @return [Siblings]
123
+ def select_keys(root: false, &block)
124
+ matches = get_nodes_by_key_filter(root: root, &block)
125
+ select_nodes do |node|
126
+ matches.include?(node)
127
+ end
128
+ end
129
+
130
+ # @return [Siblings]
131
+ def select_keys!(root: false, &block)
132
+ matches = get_nodes_by_key_filter(root: root, &block)
133
+ select_nodes! do |node|
134
+ matches.include?(node)
135
+ end
136
+ end
137
+
138
+ # @return [Set<I18n::Tasks::Data::Tree::Node>]
139
+ def get_nodes_by_key_filter(root: false, &block)
140
+ matches = Set.new
141
+ keys(root: root) do |full_key, node|
142
+ if block.yield(full_key, node)
143
+ node.walk_to_root do |p|
144
+ break unless matches.add?(p)
145
+ end
146
+ end
147
+ end
148
+ matches
149
+ end
150
+
151
+ # @return [Siblings]
152
+ def intersect_keys(other_tree, key_opts = {}, &block)
153
+ if block
154
+ select_keys(**key_opts.slice(:root)) do |key, node|
155
+ other_node = other_tree[key]
156
+ other_node && yield(key, node, other_node)
157
+ end
158
+ else
159
+ select_keys(**key_opts.slice(:root)) { |key, _node| other_tree[key] }
160
+ end
161
+ end
162
+
163
+ def grep_keys(match, opts = {})
164
+ select_keys(opts) do |full_key, _node|
165
+ match === full_key # rubocop:disable Style/CaseEquality
166
+ end
167
+ end
168
+
169
+ def set_each_value!(val_pattern, key_pattern = nil, &value_proc)
170
+ value_proc ||= proc do |node|
171
+ node_value = node.value
172
+ next node_value if node.reference?
173
+
174
+ human_key = ActiveSupport::Inflector.humanize(node.key.to_s)
175
+ full_key = node.full_key
176
+ default = (node.data[:occurrences] || []).detect { |o| o.default_arg.presence }.try(:default_arg)
177
+ StringInterpolation.interpolate_soft(
178
+ val_pattern,
179
+ value: node_value,
180
+ human_key: human_key,
181
+ key: full_key,
182
+ default: default,
183
+ value_or_human_key: node_value.presence || human_key,
184
+ value_or_default_or_human_key: node_value.presence || default || human_key
185
+ )
186
+ end
187
+ pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(key_pattern) if key_pattern.present?
188
+ keys.each do |key, node|
189
+ next if pattern_re && key !~ pattern_re
190
+
191
+ node.value = value_proc.call(node)
192
+ end
193
+ self
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/data/file_system'
4
+
5
+ module I18n::Tasks
6
+ module Data
7
+ DATA_DEFAULTS = {
8
+ adapter: 'I18n::Tasks::Data::FileSystem'
9
+ }.freeze
10
+
11
+ # I18n data provider
12
+ # @see I18n::Tasks::Data::FileSystem
13
+ def data
14
+ @data ||= begin
15
+ data_config = (config[:data] || {}).deep_symbolize_keys
16
+ data_config[:base_locale] = base_locale
17
+ data_config[:locales] = config[:locales]
18
+ adapter_class = data_config[:adapter].presence || data_config[:class].presence || DATA_DEFAULTS[:adapter]
19
+ adapter_class = adapter_class.to_s
20
+ adapter_class = 'I18n::Tasks::Data::FileSystem' if adapter_class == 'file_system'
21
+ data_config.except!(:adapter, :class)
22
+ ActiveSupport::Inflector.constantize(adapter_class).new data_config
23
+ end
24
+ end
25
+
26
+ def empty_forest
27
+ ::I18n::Tasks::Data::Tree::Siblings.new
28
+ end
29
+
30
+ def data_forest(locales = self.locales)
31
+ locales.inject(empty_forest) do |tree, locale|
32
+ tree.merge! data[locale]
33
+ end
34
+ end
35
+
36
+ def t(key, locale = base_locale)
37
+ data.t(key, locale)
38
+ end
39
+
40
+ def tree(sel)
41
+ data[split_key(sel, 2).first][sel].try(:children)
42
+ end
43
+
44
+ def node(key, locale = base_locale)
45
+ data[locale]["#{locale}.#{key}"]
46
+ end
47
+
48
+ def build_tree(hash)
49
+ I18n::Tasks::Data::Tree::Siblings.from_nested_hash(hash)
50
+ end
51
+
52
+ def t_proc(locale = base_locale)
53
+ @t_proc ||= {}
54
+ @t_proc[locale] ||= proc { |key| t(key, locale) }
55
+ end
56
+
57
+ # whether the value for key exists in locale (defaults: base_locale)
58
+ def key_value?(key, locale = base_locale)
59
+ !t(key, locale).nil?
60
+ end
61
+
62
+ def external_key?(key, locale = base_locale)
63
+ data.external(locale)[locale.to_s][key]
64
+ end
65
+
66
+ # Normalize all the locale data in the store (by writing to the store).
67
+ #
68
+ # @param [Array<String>] locales locales to normalize. Default: all.
69
+ # @param [Boolean] force_pattern_router Whether to use pattern router regardless of the config.
70
+ def normalize_store!(locales: nil, force_pattern_router: false)
71
+ locales ||= self.locales
72
+ router = force_pattern_router ? ::I18n::Tasks::Data::Router::PatternRouter.new(data, data.config) : data.router
73
+ data.with_router(router) do
74
+ Array(locales).each do |target_locale|
75
+ # The store handles actual normalization:
76
+ data[target_locale] = data[target_locale]
77
+ end
78
+ end
79
+ end
80
+
81
+ # @param [Array<String>] locales locales to check. Default: all.
82
+ # @return [Array<String>] paths to data that requires normalization
83
+ def non_normalized_paths(locales: nil)
84
+ Array(locales || self.locales).flat_map { |locale| data.non_normalized_paths(locale) }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n::Tasks
4
+ module HtmlKeys
5
+ HTML_KEY_PATTERN = /[.\-_]html\z/.freeze
6
+ MAYBE_PLURAL_HTML_KEY_PATTERN = /[.\-_]html\.[^.]+\z/.freeze
7
+
8
+ def html_key?(full_key, locale)
9
+ !!(full_key =~ HTML_KEY_PATTERN ||
10
+ full_key =~ MAYBE_PLURAL_HTML_KEY_PATTERN &&
11
+ depluralize_key(split_key(full_key, 2)[1], locale) =~ HTML_KEY_PATTERN)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n::Tasks::IgnoreKeys
4
+ # whether to ignore the key
5
+ # will also apply global ignore rules
6
+ # @param [:missing, :unused, :eq_base] ignore_type
7
+ def ignore_key?(key, ignore_type, locale = nil)
8
+ key =~ ignore_pattern(ignore_type, locale)
9
+ end
10
+
11
+ # @param type [nil, :missing, :unused, :eq_base] type
12
+ # @param locale [String] only when type is :eq_base
13
+ # @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
14
+ def ignore_pattern(type, locale = nil)
15
+ @ignore_patterns ||= HashWithIndifferentAccess.new
16
+ @ignore_patterns[type] ||= {}
17
+ @ignore_patterns[type][locale] ||= begin
18
+ global = ignore_config.presence || []
19
+ type_ignore = ignore_config(type).presence || []
20
+ patterns = case type_ignore
21
+ when Array
22
+ global + type_ignore
23
+ when Hash
24
+ # ignore per locale
25
+ global + (type_ignore['all'] || []) +
26
+ type_ignore.select { |k, _v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
27
+ end
28
+ compile_patterns_re patterns
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n::Tasks
4
+ module Interpolations
5
+ VARIABLE_REGEX = /%{[^}]+}/.freeze
6
+
7
+ def inconsistent_interpolations(locales: nil, base_locale: nil) # rubocop:disable Metrics/AbcSize
8
+ locales ||= self.locales
9
+ base_locale ||= self.base_locale
10
+ result = empty_forest
11
+
12
+ data[base_locale].key_values.each do |key, value|
13
+ next if !value.is_a?(String) || ignore_key?(key, :inconsistent_interpolations)
14
+
15
+ base_vars = Set.new(value.scan(VARIABLE_REGEX))
16
+ (locales - [base_locale]).each do |current_locale|
17
+ node = data[current_locale].first.children[key]
18
+ next unless node&.value.is_a?(String)
19
+
20
+ if base_vars != Set.new(node.value.scan(VARIABLE_REGEX))
21
+ result.merge!(node.walk_to_root.reduce(nil) { |c, p| [p.derive(children: c)] })
22
+ end
23
+ end
24
+ end
25
+
26
+ result.each { |root| root.data[:type] = :inconsistent_interpolations }
27
+ result
28
+ end
29
+ end
30
+ end