i18n-processes 0.1.0

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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile.lock +102 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +46 -0
  5. data/Rakefile +12 -0
  6. data/bin/i18n-processes +28 -0
  7. data/bin/i18n-processes.cmd +2 -0
  8. data/config/locales/en.yml +2 -0
  9. data/config/locales/zh-CN.yml +2 -0
  10. data/i18n-processes.gemspec +64 -0
  11. data/lib/i18n/processes/base_process.rb +47 -0
  12. data/lib/i18n/processes/cli.rb +208 -0
  13. data/lib/i18n/processes/command/collection.rb +21 -0
  14. data/lib/i18n/processes/command/commander.rb +43 -0
  15. data/lib/i18n/processes/command/commands/data.rb +107 -0
  16. data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
  17. data/lib/i18n/processes/command/commands/health.rb +26 -0
  18. data/lib/i18n/processes/command/commands/meta.rb +38 -0
  19. data/lib/i18n/processes/command/commands/missing.rb +86 -0
  20. data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
  21. data/lib/i18n/processes/command/commands/tree.rb +119 -0
  22. data/lib/i18n/processes/command/commands/usages.rb +69 -0
  23. data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
  24. data/lib/i18n/processes/command/dsl.rb +56 -0
  25. data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
  26. data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
  27. data/lib/i18n/processes/command/options/common.rb +41 -0
  28. data/lib/i18n/processes/command/options/data.rb +95 -0
  29. data/lib/i18n/processes/command/options/locales.rb +36 -0
  30. data/lib/i18n/processes/command_error.rb +13 -0
  31. data/lib/i18n/processes/commands.rb +31 -0
  32. data/lib/i18n/processes/configuration.rb +132 -0
  33. data/lib/i18n/processes/console_context.rb +76 -0
  34. data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
  35. data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
  36. data/lib/i18n/processes/data/file_formats.rb +111 -0
  37. data/lib/i18n/processes/data/file_system.rb +14 -0
  38. data/lib/i18n/processes/data/file_system_base.rb +205 -0
  39. data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
  40. data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
  41. data/lib/i18n/processes/data/tree/node.rb +204 -0
  42. data/lib/i18n/processes/data/tree/nodes.rb +97 -0
  43. data/lib/i18n/processes/data/tree/siblings.rb +333 -0
  44. data/lib/i18n/processes/data/tree/traversal.rb +190 -0
  45. data/lib/i18n/processes/data.rb +87 -0
  46. data/lib/i18n/processes/google_translation.rb +125 -0
  47. data/lib/i18n/processes/html_keys.rb +16 -0
  48. data/lib/i18n/processes/ignore_keys.rb +30 -0
  49. data/lib/i18n/processes/key_pattern_matching.rb +37 -0
  50. data/lib/i18n/processes/locale_list.rb +19 -0
  51. data/lib/i18n/processes/locale_pathname.rb +17 -0
  52. data/lib/i18n/processes/logging.rb +37 -0
  53. data/lib/i18n/processes/missing_keys.rb +122 -0
  54. data/lib/i18n/processes/path.rb +42 -0
  55. data/lib/i18n/processes/plural_keys.rb +41 -0
  56. data/lib/i18n/processes/rainbow_utils.rb +13 -0
  57. data/lib/i18n/processes/references.rb +101 -0
  58. data/lib/i18n/processes/reports/base.rb +71 -0
  59. data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
  60. data/lib/i18n/processes/reports/terminal.rb +252 -0
  61. data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
  62. data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
  63. data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
  64. data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
  65. data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
  66. data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
  67. data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
  68. data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
  69. data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
  70. data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
  71. data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
  72. data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
  73. data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
  74. data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
  75. data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
  76. data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
  77. data/lib/i18n/processes/scanners/scanner.rb +17 -0
  78. data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
  79. data/lib/i18n/processes/split_key.rb +68 -0
  80. data/lib/i18n/processes/stats.rb +24 -0
  81. data/lib/i18n/processes/string_interpolation.rb +16 -0
  82. data/lib/i18n/processes/unused_keys.rb +23 -0
  83. data/lib/i18n/processes/used_keys.rb +177 -0
  84. data/lib/i18n/processes/version.rb +7 -0
  85. data/lib/i18n/processes.rb +69 -0
  86. data/source/p1/_messages/zh/article.properties +9 -0
  87. data/source/p1/_messages/zh/company.properties +62 -0
  88. data/source/p1/_messages/zh/devices.properties +40 -0
  89. data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
  90. data/source/p1/_messages/zh/meetingBooking.properties +18 -0
  91. data/source/p1/_messages/zh/office-areas.properties +64 -0
  92. data/source/p1/_messages/zh/orders.properties +25 -0
  93. data/source/p1/_messages/zh/schedulings.properties +7 -0
  94. data/source/p1/_messages/zh/tag.properties +2 -0
  95. data/source/p1/_messages/zh/ticket.properties +9 -0
  96. data/source/p1/_messages/zh/visitor.properties +5 -0
  97. data/source/p1/messages +586 -0
  98. data/source/p2/orders.properties +25 -0
  99. data/source/p2/schedulings.properties +7 -0
  100. data/source/p2/tag.properties +2 -0
  101. data/source/p2/ticket.properties +9 -0
  102. data/source/p2/visitor.properties +5 -0
  103. data/source/zh.messages.ts +30 -0
  104. data/translated/en/p1/_messages/zh/article.properties +9 -0
  105. data/translated/en/p1/_messages/zh/company.properties +62 -0
  106. data/translated/en/p1/_messages/zh/devices.properties +40 -0
  107. data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
  108. data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
  109. data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
  110. data/translated/en/p1/_messages/zh/orders.properties +25 -0
  111. data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
  112. data/translated/en/p1/_messages/zh/tag.properties +2 -0
  113. data/translated/en/p1/_messages/zh/ticket.properties +9 -0
  114. data/translated/en/p1/_messages/zh/visitor.properties +5 -0
  115. data/translated/en/p1/messages +586 -0
  116. data/translated/en/p2/orders.properties +25 -0
  117. data/translated/en/p2/schedulings.properties +7 -0
  118. data/translated/en/p2/tag.properties +2 -0
  119. data/translated/en/p2/ticket.properties +9 -0
  120. data/translated/en/p2/visitor.properties +5 -0
  121. data/translated/en/zh.messages.ts +30 -0
  122. data/translation/en/article.properties +9 -0
  123. data/translation/en/company.properties +56 -0
  124. data/translation/en/meeting-rooms.properties +87 -0
  125. data/translation/en/meetingBooking.properties +14 -0
  126. data/translation/en/messages.en +164 -0
  127. data/translation/en/office-areas.properties +51 -0
  128. data/translation/en/orders.properties +26 -0
  129. data/translation/en/tag.properties +2 -0
  130. data/translation/en/translated +1263 -0
  131. data/translation/en/visitor.properties +4 -0
  132. metadata +408 -0
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/data/tree/traversal'
4
+ require 'i18n/processes/rainbow_utils'
5
+ module I18n::Processes::Data::Tree
6
+ # A list of nodes
7
+ class Nodes
8
+ include Enumerable
9
+ include Traversal
10
+
11
+ attr_reader :list
12
+
13
+ def initialize(opts = {})
14
+ @list = opts[:nodes] ? opts[:nodes].to_a.clone : []
15
+ end
16
+
17
+ delegate :each, :present?, :empty?, :blank?, :size, :to_a, to: :@list
18
+
19
+ def to_nodes
20
+ self
21
+ end
22
+
23
+ def attributes
24
+ { nodes: @list }
25
+ end
26
+
27
+ def derive(new_attr = {})
28
+ attr = attributes.except(:nodes, :parent).merge(new_attr)
29
+ node_attr = new_attr.slice(:parent)
30
+ attr[:nodes] ||= @list.map { |node| node.derive(node_attr) }
31
+ self.class.new(attr)
32
+ end
33
+
34
+ def to_hash(sort = false)
35
+ (@hash ||= {})[sort] ||= begin
36
+ if sort
37
+ sort_by(&:key)
38
+ else
39
+ self
40
+ end.map { |node| node.to_hash(sort) }.reduce({}, :deep_merge!)
41
+ end
42
+ end
43
+
44
+ delegate :to_json, to: :to_hash
45
+ delegate :to_yaml, to: :to_hash
46
+
47
+ def inspect
48
+ if present?
49
+ map(&:inspect) * "\n"
50
+ else
51
+ I18n::Processes::RainbowUtils.faint_color('{∅}')
52
+ end
53
+ end
54
+
55
+ # methods below change state
56
+
57
+ def remove!(node)
58
+ @list.delete(node) || fail("#{node.full_key} not found in #{inspect}")
59
+ dirty!
60
+ self
61
+ end
62
+
63
+ def append!(other)
64
+ @list += other.to_a
65
+ dirty!
66
+ self
67
+ end
68
+
69
+ def append(other)
70
+ derive.append!(other)
71
+ end
72
+
73
+ alias << append
74
+
75
+ def merge!(nodes)
76
+ @list += nodes.to_a
77
+ dirty!
78
+ self
79
+ end
80
+ alias + merge!
81
+
82
+ def children(&block)
83
+ return to_enum(:children) { map { |c| c.children ? c.children.size : 0 }.reduce(:+) } unless block
84
+ each do |node|
85
+ node.children.each(&block) if node.children?
86
+ end
87
+ end
88
+
89
+ alias children? any?
90
+
91
+ protected
92
+
93
+ def dirty!
94
+ @hash = nil
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'i18n/processes/split_key'
5
+ require 'i18n/processes/data/tree/nodes'
6
+ require 'i18n/processes/data/tree/node'
7
+
8
+ module I18n::Processes::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::Processes::SplitKey
14
+
15
+ attr_reader :parent, :key_to_node
16
+
17
+ def initialize(opts = {})
18
+ super(nodes: opts[:nodes])
19
+ @parent = opts[:parent] || first.try(:parent)
20
+ @list.map! { |node| node.parent == @parent ? node : node.derive(parent: @parent) }
21
+ @key_to_node = @list.each_with_object({}) { |node, h| h[node.key] = node }
22
+ @warn_about_add_children_to_leaf = opts.fetch(:warn_about_add_children_to_leaf, true)
23
+ end
24
+
25
+ def attributes
26
+ super.merge(parent: @parent)
27
+ end
28
+
29
+ def rename_key(key, new_key)
30
+ node = key_to_node.delete(key)
31
+ replace_node! node, node.derive(key: new_key)
32
+ self
33
+ end
34
+
35
+ def rename_each_key!(full_key_pattern, new_key_tpl)
36
+ pattern_re = I18n::Processes::KeyPatternMatching.compile_key_pattern(full_key_pattern)
37
+ nodes do |node|
38
+ next if node.full_key(root: true) !~ pattern_re
39
+ new_key = new_key_tpl.gsub('%{key}', node.key)
40
+ if node.parent == parent
41
+ rename_key(node.key, new_key)
42
+ else
43
+ node.parent.children.rename_key(node.key, new_key)
44
+ end
45
+ end
46
+ self
47
+ end
48
+
49
+ # @param from_pattern [Regexp]
50
+ # @param to_pattern [Regexp]
51
+ # @param root [Boolean]
52
+ # @return {old key => new key}
53
+ def mv_key!(from_pattern, to_pattern, root: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
54
+ moved_forest = Siblings.new
55
+ moved_nodes = []
56
+ old_key_to_new_key = {}
57
+ nodes do |node|
58
+ full_key = node.full_key(root: root)
59
+ if from_pattern =~ full_key
60
+ moved_nodes << node
61
+ if to_pattern.empty?
62
+ old_key_to_new_key[full_key] = nil
63
+ next
64
+ end
65
+ match = $~
66
+ new_key = to_pattern.gsub(/\\\d+/) { |m| match[m[1..-1].to_i] }
67
+ old_key_to_new_key[full_key] = new_key
68
+ moved_forest.merge!(Siblings.new.tap do |forest|
69
+ forest[[(node.root.try(:key) unless root), new_key].compact.join('.')] =
70
+ node.derive(key: split_key(new_key).last)
71
+ end)
72
+ end
73
+ end
74
+ # Adjust references
75
+ # TODO: support nested references better
76
+ nodes do |node|
77
+ next unless node.reference?
78
+ old_target = [(node.root.key if root), node.value.to_s].compact.join('.')
79
+ new_target = old_key_to_new_key[old_target]
80
+ if new_target
81
+ new_target = new_target.sub(/\A[^.]*\./, '') if root
82
+ node.value = new_target.to_sym
83
+ end
84
+ end
85
+ remove_nodes_and_emptied_ancestors! moved_nodes
86
+ merge! moved_forest
87
+ old_key_to_new_key
88
+ end
89
+
90
+ def replace_node!(node, new_node)
91
+ @list[@list.index(node)] = new_node
92
+ key_to_node[new_node.key] = new_node
93
+ end
94
+
95
+ # @return [Node] by full key
96
+ def get(full_key)
97
+ first_key, rest = split_key(full_key.to_s, 2)
98
+ node = key_to_node[first_key]
99
+ node = node.children.try(:get, rest) if rest && node
100
+ node
101
+ end
102
+
103
+ alias [] get
104
+
105
+ # add or replace node by full key
106
+ def set(full_key, node)
107
+ fail 'value should be a I18n::Processes::Data::Tree::Node' unless node.is_a?(Node)
108
+ key_part, rest = split_key(full_key, 2)
109
+ child = key_to_node[key_part]
110
+
111
+ if rest
112
+ unless child
113
+ child = Node.new(
114
+ key: key_part,
115
+ parent: parent,
116
+ children: [],
117
+ warn_about_add_children_to_leaf: @warn_add_children_to_leaf
118
+ )
119
+ append! child
120
+ end
121
+ unless child.children
122
+ warn_add_children_to_leaf child if @warn_about_add_children_to_leaf
123
+ child.children = []
124
+ end
125
+ child.children.set rest, node
126
+ else
127
+ remove! child if child
128
+ append! node
129
+ end
130
+ dirty!
131
+ node
132
+ end
133
+
134
+ alias []= set
135
+
136
+ # methods below change state
137
+
138
+ def remove!(node)
139
+ super
140
+ key_to_node.delete(node.key)
141
+ self
142
+ end
143
+
144
+ def append!(nodes)
145
+ nodes = nodes.map do |node|
146
+ fail "already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
147
+ key_to_node[node.key] = (node.parent == parent ? node : node.derive(parent: parent))
148
+ end
149
+ super(nodes)
150
+ self
151
+ end
152
+
153
+ def append(nodes)
154
+ derive.append!(nodes)
155
+ end
156
+
157
+ # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
158
+ def merge!(nodes, on_leaves_merge: nil)
159
+ nodes = Siblings.from_nested_hash(nodes) if nodes.is_a?(Hash)
160
+ nodes.each do |node|
161
+ merge_node! node, on_leaves_merge: on_leaves_merge
162
+ end
163
+ self
164
+ end
165
+
166
+ def merge(nodes)
167
+ derive.merge!(nodes)
168
+ end
169
+
170
+ def subtract_keys(keys)
171
+ remove_nodes_and_emptied_ancestors(find_nodes(keys))
172
+ end
173
+
174
+ def subtract_keys!(keys)
175
+ remove_nodes_and_emptied_ancestors!(find_nodes(keys))
176
+ end
177
+
178
+ def subtract_by_key(other)
179
+ subtract_keys other.key_names(root: true)
180
+ end
181
+
182
+ def subtract_by_key!(other)
183
+ subtract_keys! other.key_names(root: true)
184
+ end
185
+
186
+ def set_root_key!(new_key, data = nil)
187
+ return self if empty?
188
+ rename_key first.key, new_key
189
+ leaves { |node| node.data.merge! data } if data
190
+ self
191
+ end
192
+
193
+ # @param on_leaves_merge [Proc] invoked when a leaf is merged with another leaf
194
+ def merge_node!(node, on_leaves_merge: nil) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
195
+ if key_to_node.key?(node.key)
196
+ our = key_to_node[node.key]
197
+ return if our == node
198
+ our.value = node.value if node.leaf?
199
+ our.data.merge!(node.data) if node.data?
200
+ if node.children?
201
+ if our.children
202
+ our.children.merge!(node.children)
203
+ else
204
+ warn_add_children_to_leaf our
205
+ our.children = node.children
206
+ end
207
+ elsif on_leaves_merge
208
+ on_leaves_merge.call(our, node)
209
+ end
210
+ else
211
+ @list << (key_to_node[node.key] = node.derive(parent: parent))
212
+ dirty!
213
+ end
214
+ end
215
+
216
+ # @param nodes [Enumerable] Modified in-place.
217
+ def remove_nodes_and_emptied_ancestors(nodes)
218
+ add_ancestors_that_only_contain_nodes! nodes
219
+ select_nodes { |node| !nodes.include?(node) }
220
+ end
221
+
222
+ # @param nodes [Enumerable] Modified in-place.
223
+ def remove_nodes_and_emptied_ancestors!(nodes)
224
+ add_ancestors_that_only_contain_nodes! nodes
225
+ select_nodes! { |node| !nodes.include?(node) }
226
+ end
227
+
228
+ private
229
+
230
+ def find_nodes(keys)
231
+ keys.each_with_object(Set.new) do |key, set|
232
+ node = get(key)
233
+ set << node if node
234
+ end
235
+ end
236
+
237
+ # Adds all the ancestors that only contain the given nodes as descendants to the given nodes.
238
+ # @param nodes [Set] Modified in-place.
239
+ def add_ancestors_that_only_contain_nodes!(nodes)
240
+ levels.reverse_each do |level_nodes|
241
+ level_nodes.each { |node| nodes << node if node.children? && node.children.all? { |c| nodes.include?(c) } }
242
+ end
243
+ end
244
+
245
+ def warn_add_children_to_leaf(node)
246
+ ::I18n::Processes::Logging.log_warn "'#{node.full_key}' was a leaf, now has children (value <- scope conflict)"
247
+ end
248
+
249
+ class << self
250
+ include ::I18n::Processes::SplitKey
251
+
252
+ def null
253
+ new
254
+ end
255
+
256
+ def build_forest(opts = {}, &block)
257
+ opts[:nodes] ||= []
258
+ parse_parent_opt!(opts)
259
+ forest = Siblings.new(opts)
260
+ yield(forest) if block
261
+ # forest.parent.children = forest
262
+ forest
263
+ end
264
+
265
+ # @param key_occurrences [I18n::Processes::Scanners::KeyOccurrences]
266
+ # @return [Siblings]
267
+ def from_key_occurrences(key_occurrences)
268
+ build_forest(warn_about_add_children_to_leaf: false) do |forest|
269
+ key_occurrences.each do |key_occurrence|
270
+ forest[key_occurrence.key] = ::I18n::Processes::Data::Tree::Node.new(
271
+ key: split_key(key_occurrence.key).last,
272
+ data: { occurrences: key_occurrence.occurrences }
273
+ )
274
+ end
275
+ end
276
+ end
277
+
278
+ def from_key_attr(key_attrs, opts = {}, &block)
279
+ build_forest(opts) do |forest|
280
+ key_attrs.each do |(full_key, attr)|
281
+ fail "Invalid key #{full_key.inspect}" if full_key.end_with?('.')
282
+ node = ::I18n::Processes::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::Processes::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::Processes::CommandError, "invalid tree #{hash.inspect}" unless hash.respond_to?(:map)
304
+ opts[:nodes] = hash.map { |key, value| Node.from_key_value key, value }
305
+ Siblings.new(opts)
306
+ end
307
+
308
+ alias [] from_nested_hash
309
+
310
+ # build forest from [[Full Key, Value]]
311
+ def from_flat_pairs(pairs)
312
+ Siblings.new.tap do |siblings|
313
+ pairs.each do |full_key, value|
314
+ value.gsub!(/'|\n/, '') if value.include?("\n")
315
+ siblings[full_key] = ::I18n::Processes::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::Processes::Data::Tree::Node.new(key: opts[:parent_key]) if opts[:parent_key]
324
+ opts[:parent] = ::I18n::Processes::Data::Tree::Node.new(opts[:parent_attr]) if opts[:parent_attr]
325
+ if opts[:parent_locale]
326
+ opts[:parent] = ::I18n::Processes::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,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module I18n::Processes
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
+ nodes do |node|
16
+ visitor.yield(node) if node.leaf?
17
+ end
18
+ self
19
+ end
20
+
21
+ def levels(&block)
22
+ return to_enum(:levels) unless block
23
+ nodes = to_nodes
24
+ unless nodes.empty?
25
+ block.yield nodes
26
+ if nodes.size == 1
27
+ node = first
28
+ node.children.levels(&block) if node.children?
29
+ else
30
+ I18n::Processes::Data::Tree::Nodes.new(nodes: nodes.children).levels(&block)
31
+ end
32
+ end
33
+ self
34
+ end
35
+
36
+ def breadth_first(&visitor)
37
+ return to_enum(:breadth_first) unless visitor
38
+ levels do |nodes|
39
+ nodes.each { |node| visitor.yield(node) }
40
+ end
41
+ self
42
+ end
43
+
44
+ def depth_first(&visitor)
45
+ return to_enum(:depth_first) unless visitor
46
+ each do |node|
47
+ visitor.yield node
48
+ next unless node.children?
49
+ node.children.each do |child|
50
+ child.depth_first(&visitor)
51
+ end
52
+ end
53
+ self
54
+ end
55
+
56
+ # @option root include root in full key
57
+ def keys(root: false, &visitor)
58
+ return to_enum(:keys, root: root) unless visitor
59
+ leaves { |node| visitor.yield(node.full_key(root: root), node) }
60
+ self
61
+ end
62
+
63
+ def key_names(opts = {})
64
+ opts[:root] = false unless opts.key?(:root)
65
+ keys(opts).map { |key, _node| key }
66
+ end
67
+
68
+ def key_values(opts = {})
69
+ opts[:root] = false unless opts.key?(:root)
70
+ keys(opts).map { |key, node| [key, node.value] }
71
+ end
72
+
73
+ def root_key_values(sort = false)
74
+ result = keys(root: false).map { |key, node| [node.root.key, key, node.value] }
75
+ result.sort! { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] } if sort
76
+ result
77
+ end
78
+
79
+ def root_key_value_data(sort = false)
80
+ result = keys(root: false).map { |key, node| [node.root.key, key, node.value, node.data] }
81
+ result.sort! { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] } if sort
82
+ result
83
+ end
84
+
85
+ #-- modify / derive
86
+
87
+ # Select the nodes for which the block returns true. Pre-order traversal.
88
+ # @return [Siblings] a new tree
89
+ def select_nodes(&block)
90
+ tree = Siblings.new
91
+ each do |node|
92
+ next unless block.yield(node)
93
+ tree.append! node.derive(
94
+ parent: tree.parent,
95
+ children: (node.children.select_nodes(&block).to_a if node.children)
96
+ )
97
+ end
98
+ tree
99
+ end
100
+
101
+ # Select the nodes for which the block returns true. Pre-order traversal.
102
+ # @return [Siblings] self
103
+ def select_nodes!(&block)
104
+ to_remove = []
105
+ each do |node|
106
+ if block.yield(node)
107
+ node.children.select_nodes!(&block) if node.children
108
+ else
109
+ # removing during each is unsafe
110
+ to_remove << node
111
+ end
112
+ end
113
+ to_remove.each { |node| remove! node }
114
+ self
115
+ end
116
+
117
+ # @return [Siblings]
118
+ def select_keys(root: false, &block)
119
+ matches = get_nodes_by_key_filter(root: root, &block)
120
+ select_nodes do |node|
121
+ matches.include?(node)
122
+ end
123
+ end
124
+
125
+ # @return [Siblings]
126
+ def select_keys!(root: false, &block)
127
+ matches = get_nodes_by_key_filter(root: root, &block)
128
+ select_nodes! do |node|
129
+ matches.include?(node)
130
+ end
131
+ end
132
+
133
+ # @return [Set<I18n::Processes::Data::Tree::Node>]
134
+ def get_nodes_by_key_filter(root: false, &block)
135
+ matches = Set.new
136
+ keys(root: root) do |full_key, node|
137
+ if block.yield(full_key, node)
138
+ node.walk_to_root do |p|
139
+ break unless matches.add?(p)
140
+ end
141
+ end
142
+ end
143
+ matches
144
+ end
145
+
146
+ # @return [Siblings]
147
+ def intersect_keys(other_tree, key_opts = {}, &block)
148
+ if block
149
+ select_keys(key_opts) do |key, node|
150
+ other_node = other_tree[key]
151
+ other_node && yield(key, node, other_node)
152
+ end
153
+ else
154
+ select_keys(key_opts) { |key, _node| other_tree[key] }
155
+ end
156
+ end
157
+
158
+ def grep_keys(match, opts = {})
159
+ select_keys(opts) do |full_key, _node|
160
+ match === full_key # rubocop:disable Style/CaseEquality
161
+ end
162
+ end
163
+
164
+ def set_each_value!(val_pattern, key_pattern = nil, &value_proc)
165
+ value_proc ||= proc do |node|
166
+ node_value = node.value
167
+ next node_value if node.reference?
168
+ human_key = ActiveSupport::Inflector.humanize(node.key.to_s)
169
+ full_key = node.full_key
170
+ default = (node.data[:occurrences] || []).detect { |o| o.default_arg.presence }.try(:default_arg)
171
+ StringInterpolation.interpolate_soft(
172
+ val_pattern,
173
+ value: node_value,
174
+ human_key: human_key,
175
+ key: full_key,
176
+ default: default,
177
+ value_or_human_key: node_value.presence || human_key,
178
+ value_or_default_or_human_key: node_value.presence || default || human_key
179
+ )
180
+ end
181
+ pattern_re = I18n::Processes::KeyPatternMatching.compile_key_pattern(key_pattern) if key_pattern.present?
182
+ keys.each do |key, node|
183
+ next if pattern_re && key !~ pattern_re
184
+ node.value = value_proc.call(node)
185
+ end
186
+ self
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/data/file_system'
4
+
5
+ module I18n::Processes
6
+ module Data
7
+ DATA_DEFAULTS = {
8
+ adapter: 'I18n::Processes::Data::FileSystem'
9
+ }.freeze
10
+
11
+ # I18n data provider
12
+ # @see I18n::Processes::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::Processes::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::Processes::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::Processes::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::Processes::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