i18n-tasks 0.3.11 → 0.4.0.beta1

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.
@@ -0,0 +1,51 @@
1
+ require 'i18n/tasks/data/router/pattern_router'
2
+
3
+ module I18n::Tasks
4
+ module Data::Router
5
+ # Keep the path, or infer from base locale
6
+ class ConservativeRouter < PatternRouter
7
+ def initialize(adapter, config)
8
+ @adapter = adapter
9
+ @base_locale = config[:base_locale]
10
+ super
11
+ end
12
+
13
+ def route(locale, forest, &block)
14
+ return to_enum(:route, locale, forest) unless block
15
+ out = {}
16
+ not_found = Set.new
17
+ forest.keys(root: false) do |key, node|
18
+ locale_key = "#{locale}.#{key}"
19
+ path = adapter[locale][locale_key].data[:path]
20
+
21
+ # infer from base
22
+ unless path
23
+ path = base_tree["#{base_locale}.#{key}"].try(:data).try(:[], :path)
24
+ path = path.try :sub, /(?<=[\/.])#{base_locale}(?=\.)/, locale
25
+ end
26
+
27
+ if path
28
+ (out[path] ||= Set.new) << locale_key
29
+ else
30
+ not_found << locale_key
31
+ end
32
+ end
33
+ out.each do |dest, keys|
34
+ block.yield dest, forest.select_keys { |key, _| keys.include?(key) }
35
+ end
36
+ if not_found.present?
37
+ super(locale, forest.select_keys { |key, _| not_found.include?(key) }, &block)
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ def base_tree
44
+ adapter[base_locale]
45
+ end
46
+
47
+ attr_reader :adapter, :base_locale
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,57 @@
1
+ require 'i18n/tasks/key_pattern_matching'
2
+ require 'i18n/tasks/data/tree/node'
3
+
4
+ module I18n::Tasks
5
+ module Data::Router
6
+ # Route based on key name
7
+ class PatternRouter
8
+ include ::I18n::Tasks::KeyPatternMatching
9
+
10
+ attr_reader :routes
11
+ # @option data_config write [Array] of routes
12
+ # @example
13
+ # {write:
14
+ # # keys matched top to bottom
15
+ # [['devise.*', 'config/locales/devise.%{locale}.yml'],
16
+ # # default catch-all (same as ['*', 'config/locales/%{locale}.yml'])
17
+ # 'config/locales/%{locale}.yml']}
18
+ def initialize(_adapter, data_config)
19
+ @routes_config = data_config[:write]
20
+ @routes = compile_routes @routes_config
21
+ end
22
+
23
+ # Route keys to destinations
24
+ # @param forest [I18n::Tasks::LocaleTree::Siblings] forest roots are locales.
25
+ # @return [Hash] mapping of destination => [ [key, value], ... ]
26
+ def route(locale, forest, &block)
27
+ return to_enum(:route, locale, forest) unless block
28
+ locale = locale.to_s
29
+ out = {}
30
+ forest.keys(root: false) do |key, _node|
31
+ pattern, path = routes.detect { |route| route[0] =~ key }
32
+ if pattern
33
+ key_match = $~
34
+ path = path % {locale: locale}
35
+ path.gsub!(/\\\d+/) { |m| key_match[m[1..-1].to_i] }
36
+ (out[path] ||= Set.new) << "#{locale}.#{key}"
37
+ else
38
+ raise "no route matches key. routes = #{@routes_config}, key = #{key}"
39
+ end
40
+ end
41
+ out.each do |dest, keys|
42
+ block.yield dest,
43
+ forest.select_keys { |key, _| keys.include?(key) }
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def compile_routes(routes)
50
+ routes.map { |x| x.is_a?(String) ? ['*', x] : x }.map { |x|
51
+ [compile_key_pattern(x[0]), x[1]]
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,199 @@
1
+ require 'i18n/tasks/data/tree/traversal'
2
+ require 'i18n/tasks/data/tree/siblings'
3
+ module I18n::Tasks::Data::Tree
4
+ class Node
5
+ attr_accessor :value
6
+ attr_reader :key, :children, :parent
7
+
8
+ def initialize(key: nil, value: nil, data: nil, parent: nil, children: nil)
9
+ @key = key.try(:to_s)
10
+ @value = value
11
+ @data = data
12
+ @parent = parent
13
+ self.children = children if children
14
+ end
15
+
16
+ def attributes
17
+ {key: @key, value: @value, data: @data, parent: @parent, children: @children}
18
+ end
19
+
20
+ def derive(new_attr = {})
21
+ self.class.new(attributes.merge(new_attr))
22
+ end
23
+
24
+ def key=(value)
25
+ dirty!
26
+ @key = value.try(:to_s)
27
+ end
28
+
29
+ def children=(nodes_or_siblings)
30
+ dirty!
31
+ @children = Siblings.new(nodes: nodes_or_siblings, parent: self)
32
+ end
33
+
34
+ def each(&block)
35
+ return to_enum(:each) { 1 } unless block
36
+ [self].each(&block)
37
+ end
38
+
39
+ include Enumerable
40
+ include Traversal
41
+
42
+ # null nodes are like nil, but do not blow up and can have children
43
+ # never yielded during traversal, but are passed through and can have non-null children
44
+ def null?
45
+ key.nil?
46
+ end
47
+
48
+ def hash_or_value
49
+ leaf? ? value : children.try(:to_hash)
50
+ end
51
+
52
+ def leaf?
53
+ !children?
54
+ end
55
+
56
+ attr_writer :leaf
57
+
58
+ # a node with key nil is considered Empty. this is to allow for using these nodes instead of nils
59
+ def root?
60
+ !parent?
61
+ end
62
+
63
+ def parent?
64
+ parent && !parent.null?
65
+ end
66
+
67
+ def children?
68
+ children && children.any?
69
+ end
70
+
71
+ def data
72
+ @data ||= {}
73
+ end
74
+
75
+ def data?
76
+ @data.present?
77
+ end
78
+
79
+ # do not use directly. use parent.append(node) instead
80
+ def parent=(value)
81
+ return if @parent == value
82
+ if value
83
+ @parent.children.remove!(self) if @parent && @parent.children
84
+ @parent = value
85
+ dirty!
86
+ end
87
+ @parent
88
+ end
89
+
90
+ def siblings
91
+ parent.children
92
+ end
93
+
94
+ def get(key)
95
+ children.get(key)
96
+ end
97
+
98
+ alias [] get
99
+
100
+ # append and reparent nodes
101
+ def append!(nodes)
102
+ if nodes.any?
103
+ if @children
104
+ @children.merge!(nodes)
105
+ else
106
+ @children = Siblings.new(nodes: nodes, parent: self)
107
+ end
108
+ end
109
+ self
110
+ end
111
+
112
+ def append(nodes)
113
+ derive.append!(nodes)
114
+ end
115
+
116
+ def full_key(root: true)
117
+ @full_key ||= {}
118
+ @full_key[root] ||= "#{"#{parent.full_key(root: root)}." if parent? && (root || parent.parent?)}#{key}"
119
+ end
120
+
121
+ def walk_to_root(&visitor)
122
+ return to_enum(:walk_to_root) unless visitor
123
+ visitor.yield self unless self.null?
124
+ parent.walk_to_root &visitor if parent?
125
+ end
126
+
127
+ def walk_from_root(&visitor)
128
+ return to_enum(:walk_from_root) unless visitor
129
+ walk_to_root.reverse_each do |node|
130
+ visitor.yield node
131
+ end
132
+ end
133
+
134
+ def to_nodes
135
+ Nodes.new([self])
136
+ end
137
+
138
+ def to_hash
139
+ @hash ||= begin
140
+ children_hash = (children || {}).map(&:to_hash).reduce(:deep_merge) || {}
141
+ if null?
142
+ children_hash
143
+ elsif leaf?
144
+ {key => value}
145
+ else
146
+ {key => children_hash}
147
+ end
148
+ end
149
+ end
150
+
151
+ delegate :to_json, to: :to_hash
152
+ delegate :to_yaml, to: :to_hash
153
+
154
+ def inspect(level: 0)
155
+ label =
156
+ if null?
157
+ Term::ANSIColor.dark '∅'
158
+ else
159
+ key = Term::ANSIColor.color(1 + level % 15, self.key)
160
+ if leaf?
161
+ value = Term::ANSIColor.cyan(self.value.to_s)
162
+ "#{key}: #{value}"
163
+ else
164
+ "#{key}"
165
+ end + (self.data? ? " #{self.data}" : '')
166
+ end
167
+
168
+ r = "#{' ' * level}#{label}"
169
+ if children?
170
+ r += "\n" + children.map { |c| c.inspect(level: level + 1) }.join("\n") if children?
171
+ end
172
+ r
173
+ end
174
+
175
+ protected
176
+
177
+ def dirty!
178
+ @hash = nil
179
+ @full_key = nil
180
+ end
181
+
182
+ class << self
183
+ def null
184
+ new
185
+ end
186
+
187
+ # value can be a nested hash
188
+ def from_key_value(key, value)
189
+ Node.new(key: key).tap do |node|
190
+ if value.is_a?(Hash)
191
+ node.children = Siblings.from_nested_hash(value, parent: node)
192
+ else
193
+ node.value = value
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,85 @@
1
+ require 'i18n/tasks/data/tree/traversal'
2
+ module I18n::Tasks::Data::Tree
3
+ # A list of nodes
4
+ class Nodes
5
+ attr_reader :list
6
+
7
+ def initialize(nodes: [])
8
+ @list = nodes.to_a.clone
9
+ end
10
+
11
+ delegate :each, :present?, :empty?, :blank?, :size, :to_a, to: :@list
12
+ include Enumerable
13
+ include Traversal
14
+
15
+ def to_nodes
16
+ self
17
+ end
18
+
19
+ def attributes
20
+ {nodes: @list}
21
+ end
22
+
23
+ def derive(new_attr = {})
24
+ attr = attributes.merge(new_attr)
25
+ attr[:nodes] ||= @list.map(&:derive)
26
+ self.class.new(attr)
27
+ end
28
+
29
+ def to_hash
30
+ @hash ||= map(&:to_hash).reduce(:deep_merge!) || {}
31
+ end
32
+
33
+ delegate :to_json, to: :to_hash
34
+ delegate :to_yaml, to: :to_hash
35
+
36
+ def inspect
37
+ if present?
38
+ map(&:inspect) * "\n"
39
+ else
40
+ Term::ANSIColor.dark '∅'
41
+ end
42
+ end
43
+
44
+ # methods below change state
45
+
46
+ def remove!(node)
47
+ @list.delete(node) or raise "#{node.full_key} not found in #{self.inspect}"
48
+ dirty!
49
+ self
50
+ end
51
+
52
+ def append!(other)
53
+ @list += other.to_a
54
+ dirty!
55
+ self
56
+ end
57
+
58
+ def append(other)
59
+ derive.append!(other)
60
+ end
61
+
62
+ alias << append
63
+
64
+ def merge!(nodes)
65
+ @list += nodes.to_a
66
+ dirty!
67
+ self
68
+ end
69
+ alias + merge!
70
+
71
+ def children(&block)
72
+ return to_enum(:children) { map { |c| c.children.size }.reduce(:+) } unless block
73
+ each do |node|
74
+ node.children.each(&block) if node.children?
75
+ end
76
+ end
77
+
78
+ alias children? any?
79
+
80
+ protected
81
+ def dirty!
82
+ @hash = nil
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,131 @@
1
+ require 'i18n/tasks/data/tree/traversal'
2
+ require 'i18n/tasks/data/tree/nodes'
3
+ module I18n::Tasks::Data::Tree
4
+ # Siblings represents a subtree sharing a common parent
5
+ # in case of an empty parent (nil) it represents a forest
6
+ # siblings' keys are unique
7
+ class Siblings < Nodes
8
+ attr_reader :parent, :key_to_node
9
+
10
+ def initialize(nodes: [], parent: nil)
11
+ super(nodes: nodes)
12
+ @key_to_node = siblings.inject({}) { |h, node| h[node.key] = node; h }
13
+ @parent = first.try(:parent) || Node.null
14
+ self.parent = parent if parent
15
+ end
16
+
17
+ def attributes
18
+ super.merge(parent: @parent)
19
+ end
20
+
21
+ def parent=(node)
22
+ return if @parent == node
23
+ each { |n| n.parent = node }
24
+ @parent = node
25
+ end
26
+
27
+ alias siblings each
28
+
29
+ # @return [Node] by full key
30
+ def get(full_key)
31
+ first_key, rest = full_key.to_s.split('.', 2)
32
+ node = key_to_node[first_key]
33
+ if rest && node
34
+ node = node.children.try(:get, rest)
35
+ end
36
+ node
37
+ end
38
+
39
+ alias [] get
40
+
41
+ # add or replace node by full key
42
+ def set(full_key, node)
43
+ key_part, rest = full_key.split('.', 2)
44
+ child = key_to_node[key_part]
45
+ if rest
46
+ unless child
47
+ child = Node.new(key: key_part)
48
+ append! child
49
+ end
50
+ child.children ||= []
51
+ child.children.set rest, node
52
+ dirty!
53
+ else
54
+ remove! child if child
55
+ append! node
56
+ end
57
+ node
58
+ end
59
+
60
+ alias []= set
61
+
62
+
63
+ # methods below change state
64
+
65
+ def remove!(node)
66
+ super
67
+ key_to_node.delete(node.key)
68
+ self
69
+ end
70
+
71
+ def append!(nodes)
72
+ nodes.each do |node|
73
+ raise "node '#{node.full_key}' already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
74
+ key_to_node[node.key] = node
75
+ node.parent = parent
76
+ end
77
+ super
78
+ self
79
+ end
80
+
81
+ def append(nodes)
82
+ derive.append!(nodes)
83
+ end
84
+
85
+ def merge!(nodes)
86
+ nodes = Siblings.from_nested_hash(nodes) if nodes.is_a?(Hash)
87
+ nodes.each do |node|
88
+ if key_to_node.key?(node.key)
89
+ our = key_to_node[node.key]
90
+ next if our == node
91
+ our.value = node.value if node.leaf?
92
+ our.data.merge!(node.data) if node.data?
93
+ our.children.merge!(node.children) if node.children?
94
+ else
95
+ key_to_node[node.key] = node.derive(parent: parent)
96
+ end
97
+ end
98
+ @list = key_to_node.values
99
+ dirty!
100
+ self
101
+ end
102
+
103
+ def merge(nodes)
104
+ derive.merge!(nodes)
105
+ end
106
+
107
+ class << self
108
+ def null
109
+ new
110
+ end
111
+
112
+ # build forest from nested hash, e.g. {'es' => { 'common' => { name => 'Nombre', 'age' => 'Edad' } } }
113
+ # this is the native i18n gem format
114
+ def from_nested_hash(hash, parent: Node.null)
115
+ Siblings.new nodes: hash.map { |key, value| Node.from_key_value key, value },
116
+ parent: parent
117
+ end
118
+
119
+ alias [] from_nested_hash
120
+
121
+ # build forest from [[Full Key, Value]]
122
+ def from_flat_pairs(pairs)
123
+ Siblings.new.tap do |siblings|
124
+ pairs.each { |full_key, value|
125
+ siblings[full_key] = Node.new(key: full_key.split('.')[-1], value: value)
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end