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.
- checksums.yaml +4 -4
- data/.jrubyrc +2 -0
- data/.travis.yml +5 -2
- data/CHANGES.md +8 -0
- data/Gemfile +1 -9
- data/README.md +36 -3
- data/Rakefile +6 -0
- data/bin/i18n-tasks +3 -1
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +1 -0
- data/i18n-tasks.gemspec +2 -2
- data/lib/i18n/tasks.rb +9 -5
- data/lib/i18n/tasks/base_task.rb +2 -4
- data/lib/i18n/tasks/commands.rb +9 -0
- data/lib/i18n/tasks/data.rb +77 -60
- data/lib/i18n/tasks/data/file_formats.rb +3 -0
- data/lib/i18n/tasks/data/file_system_base.rb +53 -30
- data/lib/i18n/tasks/data/router/conservative_router.rb +51 -0
- data/lib/i18n/tasks/data/router/pattern_router.rb +57 -0
- data/lib/i18n/tasks/data/tree/node.rb +199 -0
- data/lib/i18n/tasks/data/tree/nodes.rb +85 -0
- data/lib/i18n/tasks/data/tree/siblings.rb +131 -0
- data/lib/i18n/tasks/data/tree/traversal.rb +81 -0
- data/lib/i18n/tasks/file_structure.rb +18 -0
- data/lib/i18n/tasks/missing_keys.rb +7 -7
- data/lib/i18n/tasks/plural_keys.rb +5 -5
- data/lib/i18n/tasks/unused_keys.rb +5 -6
- data/lib/i18n/tasks/version.rb +1 -1
- data/spec/conservative_router_spec.rb +50 -0
- data/spec/file_system_data_spec.rb +5 -4
- data/spec/google_translate_spec.rb +2 -2
- data/spec/i18n_tasks_spec.rb +1 -1
- data/spec/locale_tree/siblings_spec.rb +29 -0
- data/spec/plural_keys_spec.rb +3 -2
- data/spec/spec_helper.rb +1 -0
- data/spec/support/trees.rb +5 -0
- metadata +25 -11
- data/lib/i18n/tasks/data/locale_tree.rb +0 -86
- data/lib/i18n/tasks/data/router.rb +0 -47
@@ -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
|