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.
- checksums.yaml +7 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +12 -0
- data/bin/i18n-processes +28 -0
- data/bin/i18n-processes.cmd +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/zh-CN.yml +2 -0
- data/i18n-processes.gemspec +64 -0
- data/lib/i18n/processes/base_process.rb +47 -0
- data/lib/i18n/processes/cli.rb +208 -0
- data/lib/i18n/processes/command/collection.rb +21 -0
- data/lib/i18n/processes/command/commander.rb +43 -0
- data/lib/i18n/processes/command/commands/data.rb +107 -0
- data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
- data/lib/i18n/processes/command/commands/health.rb +26 -0
- data/lib/i18n/processes/command/commands/meta.rb +38 -0
- data/lib/i18n/processes/command/commands/missing.rb +86 -0
- data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
- data/lib/i18n/processes/command/commands/tree.rb +119 -0
- data/lib/i18n/processes/command/commands/usages.rb +69 -0
- data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
- data/lib/i18n/processes/command/dsl.rb +56 -0
- data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
- data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
- data/lib/i18n/processes/command/options/common.rb +41 -0
- data/lib/i18n/processes/command/options/data.rb +95 -0
- data/lib/i18n/processes/command/options/locales.rb +36 -0
- data/lib/i18n/processes/command_error.rb +13 -0
- data/lib/i18n/processes/commands.rb +31 -0
- data/lib/i18n/processes/configuration.rb +132 -0
- data/lib/i18n/processes/console_context.rb +76 -0
- data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
- data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
- data/lib/i18n/processes/data/file_formats.rb +111 -0
- data/lib/i18n/processes/data/file_system.rb +14 -0
- data/lib/i18n/processes/data/file_system_base.rb +205 -0
- data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
- data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
- data/lib/i18n/processes/data/tree/node.rb +204 -0
- data/lib/i18n/processes/data/tree/nodes.rb +97 -0
- data/lib/i18n/processes/data/tree/siblings.rb +333 -0
- data/lib/i18n/processes/data/tree/traversal.rb +190 -0
- data/lib/i18n/processes/data.rb +87 -0
- data/lib/i18n/processes/google_translation.rb +125 -0
- data/lib/i18n/processes/html_keys.rb +16 -0
- data/lib/i18n/processes/ignore_keys.rb +30 -0
- data/lib/i18n/processes/key_pattern_matching.rb +37 -0
- data/lib/i18n/processes/locale_list.rb +19 -0
- data/lib/i18n/processes/locale_pathname.rb +17 -0
- data/lib/i18n/processes/logging.rb +37 -0
- data/lib/i18n/processes/missing_keys.rb +122 -0
- data/lib/i18n/processes/path.rb +42 -0
- data/lib/i18n/processes/plural_keys.rb +41 -0
- data/lib/i18n/processes/rainbow_utils.rb +13 -0
- data/lib/i18n/processes/references.rb +101 -0
- data/lib/i18n/processes/reports/base.rb +71 -0
- data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
- data/lib/i18n/processes/reports/terminal.rb +252 -0
- data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
- data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
- data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
- data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
- data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
- data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
- data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
- data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
- data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
- data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
- data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
- data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
- data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
- data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
- data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
- data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
- data/lib/i18n/processes/scanners/scanner.rb +17 -0
- data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
- data/lib/i18n/processes/split_key.rb +68 -0
- data/lib/i18n/processes/stats.rb +24 -0
- data/lib/i18n/processes/string_interpolation.rb +16 -0
- data/lib/i18n/processes/unused_keys.rb +23 -0
- data/lib/i18n/processes/used_keys.rb +177 -0
- data/lib/i18n/processes/version.rb +7 -0
- data/lib/i18n/processes.rb +69 -0
- data/source/p1/_messages/zh/article.properties +9 -0
- data/source/p1/_messages/zh/company.properties +62 -0
- data/source/p1/_messages/zh/devices.properties +40 -0
- data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
- data/source/p1/_messages/zh/meetingBooking.properties +18 -0
- data/source/p1/_messages/zh/office-areas.properties +64 -0
- data/source/p1/_messages/zh/orders.properties +25 -0
- data/source/p1/_messages/zh/schedulings.properties +7 -0
- data/source/p1/_messages/zh/tag.properties +2 -0
- data/source/p1/_messages/zh/ticket.properties +9 -0
- data/source/p1/_messages/zh/visitor.properties +5 -0
- data/source/p1/messages +586 -0
- data/source/p2/orders.properties +25 -0
- data/source/p2/schedulings.properties +7 -0
- data/source/p2/tag.properties +2 -0
- data/source/p2/ticket.properties +9 -0
- data/source/p2/visitor.properties +5 -0
- data/source/zh.messages.ts +30 -0
- data/translated/en/p1/_messages/zh/article.properties +9 -0
- data/translated/en/p1/_messages/zh/company.properties +62 -0
- data/translated/en/p1/_messages/zh/devices.properties +40 -0
- data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
- data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
- data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
- data/translated/en/p1/_messages/zh/orders.properties +25 -0
- data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
- data/translated/en/p1/_messages/zh/tag.properties +2 -0
- data/translated/en/p1/_messages/zh/ticket.properties +9 -0
- data/translated/en/p1/_messages/zh/visitor.properties +5 -0
- data/translated/en/p1/messages +586 -0
- data/translated/en/p2/orders.properties +25 -0
- data/translated/en/p2/schedulings.properties +7 -0
- data/translated/en/p2/tag.properties +2 -0
- data/translated/en/p2/ticket.properties +9 -0
- data/translated/en/p2/visitor.properties +5 -0
- data/translated/en/zh.messages.ts +30 -0
- data/translation/en/article.properties +9 -0
- data/translation/en/company.properties +56 -0
- data/translation/en/meeting-rooms.properties +87 -0
- data/translation/en/meetingBooking.properties +14 -0
- data/translation/en/messages.en +164 -0
- data/translation/en/office-areas.properties +51 -0
- data/translation/en/orders.properties +26 -0
- data/translation/en/tag.properties +2 -0
- data/translation/en/translated +1263 -0
- data/translation/en/visitor.properties +4 -0
- 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
|