i18n-tasks 0.6.3 → 0.7.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 +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +2 -1
- data/README.md +80 -78
- data/bin/i18n-tasks +24 -30
- data/config/i18n-tasks.yml +87 -0
- data/config/locales/en.yml +95 -0
- data/i18n-tasks.gemspec +1 -0
- data/lib/i18n/tasks.rb +10 -0
- data/lib/i18n/tasks/base_task.rb +6 -2
- data/lib/i18n/tasks/command/collection.rb +18 -0
- data/lib/i18n/tasks/command/commander.rb +72 -0
- data/lib/i18n/tasks/command/commands/data.rb +73 -0
- data/lib/i18n/tasks/command/commands/eq_base.rb +20 -0
- data/lib/i18n/tasks/command/commands/health.rb +26 -0
- data/lib/i18n/tasks/command/commands/meta.rb +35 -0
- data/lib/i18n/tasks/command/commands/missing.rb +73 -0
- data/lib/i18n/tasks/command/commands/tree.rb +92 -0
- data/lib/i18n/tasks/command/commands/usages.rb +70 -0
- data/lib/i18n/tasks/command/commands/xlsx.rb +27 -0
- data/lib/i18n/tasks/command/dsl.rb +27 -0
- data/lib/i18n/tasks/command/dsl/cmd.rb +19 -0
- data/lib/i18n/tasks/command/dsl/cmd_opt.rb +19 -0
- data/lib/i18n/tasks/command/dsl/enum_opt.rb +26 -0
- data/lib/i18n/tasks/command/options/common.rb +48 -0
- data/lib/i18n/tasks/command/options/enum_opt.rb +44 -0
- data/lib/i18n/tasks/command/options/list_opt.rb +11 -0
- data/lib/i18n/tasks/command/options/locales.rb +47 -0
- data/lib/i18n/tasks/command/options/trees.rb +101 -0
- data/lib/i18n/tasks/command_error.rb +3 -0
- data/lib/i18n/tasks/commands.rb +22 -169
- data/lib/i18n/tasks/configuration.rb +1 -16
- data/lib/i18n/tasks/console_context.rb +1 -1
- data/lib/i18n/tasks/data.rb +13 -8
- data/lib/i18n/tasks/data/file_formats.rb +29 -18
- data/lib/i18n/tasks/data/file_system_base.rb +35 -4
- data/lib/i18n/tasks/data/router/conservative_router.rb +18 -11
- data/lib/i18n/tasks/data/tree/node.rb +5 -15
- data/lib/i18n/tasks/data/tree/nodes.rb +0 -3
- data/lib/i18n/tasks/data/tree/siblings.rb +32 -2
- data/lib/i18n/tasks/data/tree/traversal.rb +117 -96
- data/lib/i18n/tasks/google_translation.rb +25 -25
- data/lib/i18n/tasks/html_keys.rb +10 -0
- data/lib/i18n/tasks/key_pattern_matching.rb +1 -0
- data/lib/i18n/tasks/locale_list.rb +19 -0
- data/lib/i18n/tasks/missing_keys.rb +32 -33
- data/lib/i18n/tasks/plural_keys.rb +1 -1
- data/lib/i18n/tasks/reports/base.rb +4 -9
- data/lib/i18n/tasks/reports/spreadsheet.rb +5 -5
- data/lib/i18n/tasks/reports/terminal.rb +62 -38
- data/lib/i18n/tasks/scanners/base_scanner.rb +5 -4
- data/lib/i18n/tasks/slop_command.rb +27 -0
- data/lib/i18n/tasks/stats.rb +20 -0
- data/lib/i18n/tasks/string_interpolation.rb +14 -0
- data/lib/i18n/tasks/unused_keys.rb +0 -10
- data/lib/i18n/tasks/version.rb +1 -1
- data/spec/commands/data_commands_spec.rb +38 -0
- data/spec/commands/tree_commands_spec.rb +68 -0
- data/spec/fixtures/app/views/index.html.slim +1 -0
- data/spec/google_translate_spec.rb +5 -3
- data/spec/i18n_spec.rb +18 -0
- data/spec/i18n_tasks_spec.rb +8 -8
- data/spec/spec_helper.rb +3 -3
- data/spec/support/test_codebase.rb +4 -1
- data/spec/used_keys_spec.rb +7 -7
- data/templates/config/i18n-tasks.yml +2 -2
- metadata +48 -4
- data/lib/i18n/tasks/commands_base.rb +0 -107
- data/lib/i18n/tasks/fill_tasks.rb +0 -40
@@ -16,7 +16,7 @@ module I18n::Tasks::Data::Tree
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def attributes
|
19
|
-
{key: @key, value: @value, data: @data, parent: @parent, children: @children}
|
19
|
+
{key: @key, value: @value, data: @data.try(:clone), parent: @parent, children: @children}
|
20
20
|
end
|
21
21
|
|
22
22
|
def derive(new_attr = {})
|
@@ -44,12 +44,6 @@ module I18n::Tasks::Data::Tree
|
|
44
44
|
include Enumerable
|
45
45
|
include Traversal
|
46
46
|
|
47
|
-
# null nodes are like nil, but do not blow up and can have children
|
48
|
-
# never yielded during traversal, but are passed through and can have non-null children
|
49
|
-
def null?
|
50
|
-
key.nil?
|
51
|
-
end
|
52
|
-
|
53
47
|
def value_or_children_hash
|
54
48
|
leaf? ? value : children.try(:to_hash)
|
55
49
|
end
|
@@ -64,7 +58,7 @@ module I18n::Tasks::Data::Tree
|
|
64
58
|
end
|
65
59
|
|
66
60
|
def parent?
|
67
|
-
parent
|
61
|
+
!!parent
|
68
62
|
end
|
69
63
|
|
70
64
|
def children?
|
@@ -107,7 +101,7 @@ module I18n::Tasks::Data::Tree
|
|
107
101
|
|
108
102
|
def walk_to_root(&visitor)
|
109
103
|
return to_enum(:walk_to_root) unless visitor
|
110
|
-
visitor.yield self
|
104
|
+
visitor.yield self
|
111
105
|
parent.walk_to_root &visitor if parent?
|
112
106
|
end
|
113
107
|
|
@@ -135,7 +129,7 @@ module I18n::Tasks::Data::Tree
|
|
135
129
|
def to_hash
|
136
130
|
@hash ||= begin
|
137
131
|
children_hash = (children || {}).map(&:to_hash).reduce(:deep_merge) || {}
|
138
|
-
if
|
132
|
+
if key.nil?
|
139
133
|
children_hash
|
140
134
|
elsif leaf?
|
141
135
|
{key => value}
|
@@ -149,7 +143,7 @@ module I18n::Tasks::Data::Tree
|
|
149
143
|
delegate :to_yaml, to: :to_hash
|
150
144
|
|
151
145
|
def inspect(level = 0)
|
152
|
-
if
|
146
|
+
if key.nil?
|
153
147
|
label = Term::ANSIColor.dark '∅'
|
154
148
|
else
|
155
149
|
label = [Term::ANSIColor.color(1 + level % 15, self.key),
|
@@ -167,10 +161,6 @@ module I18n::Tasks::Data::Tree
|
|
167
161
|
end
|
168
162
|
|
169
163
|
class << self
|
170
|
-
def null
|
171
|
-
new
|
172
|
-
end
|
173
|
-
|
174
164
|
# value can be a nested hash
|
175
165
|
def from_key_value(key, value)
|
176
166
|
Node.new(key: key.try(:to_s)).tap do |node|
|
@@ -24,9 +24,6 @@ module I18n::Tasks::Data::Tree
|
|
24
24
|
|
25
25
|
def derive(new_attr = {})
|
26
26
|
attr = attributes.except(:nodes, :parent).merge(new_attr)
|
27
|
-
if self.parent
|
28
|
-
new_attr[:parent] ||= Node.null
|
29
|
-
end
|
30
27
|
node_attr = new_attr.slice(:parent)
|
31
28
|
attr[:nodes] ||= @list.map { |node| node.derive(node_attr) }
|
32
29
|
self.class.new(attr)
|
@@ -26,6 +26,20 @@ module I18n::Tasks::Data::Tree
|
|
26
26
|
self
|
27
27
|
end
|
28
28
|
|
29
|
+
def rename_each_key!(full_key_pattern, new_key_tpl)
|
30
|
+
pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(full_key_pattern)
|
31
|
+
nodes do |node|
|
32
|
+
next if node.full_key(root: true) !~ pattern_re
|
33
|
+
new_key = new_key_tpl.gsub('%{key}', node.key)
|
34
|
+
if node.parent == parent
|
35
|
+
rename_key(node.key, new_key)
|
36
|
+
else
|
37
|
+
node.parent.children.rename_key(node.key, new_key)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
29
43
|
def replace_node!(node, new_node)
|
30
44
|
@list[@list.index(node)] = new_node
|
31
45
|
key_to_node[new_node.key] = new_node
|
@@ -122,7 +136,23 @@ module I18n::Tasks::Data::Tree
|
|
122
136
|
derive.merge!(nodes)
|
123
137
|
end
|
124
138
|
|
125
|
-
def
|
139
|
+
def subtract_keys(keys)
|
140
|
+
exclude = {}
|
141
|
+
keys.each do |full_key|
|
142
|
+
if (node = get full_key)
|
143
|
+
exclude[node] = true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
select_nodes { |node|
|
147
|
+
not exclude[node] || node.children.try(:all?) { |c| exclude[c] }
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def subtract_by_key(other)
|
152
|
+
subtract_keys other.key_names(root: true)
|
153
|
+
end
|
154
|
+
|
155
|
+
def set_root_key!(new_key, data = nil)
|
126
156
|
return self if empty?
|
127
157
|
rename_key first.key, new_key
|
128
158
|
leaves { |node| node.data.merge! data } if data
|
@@ -176,6 +206,7 @@ module I18n::Tasks::Data::Tree
|
|
176
206
|
# this is the native i18n gem format
|
177
207
|
def from_nested_hash(hash, opts = {})
|
178
208
|
parse_parent_opt!(opts)
|
209
|
+
raise ::I18n::Tasks::CommandError.new("invalid tree #{hash.inspect}") unless hash.respond_to?(:map)
|
179
210
|
opts[:nodes] = hash.map { |key, value| Node.from_key_value key, value }
|
180
211
|
Siblings.new(opts)
|
181
212
|
end
|
@@ -196,7 +227,6 @@ module I18n::Tasks::Data::Tree
|
|
196
227
|
opts[:parent] = Node.new(key: opts[:parent_key]) if opts[:parent_key]
|
197
228
|
opts[:parent] = Node.new(opts[:parent_attr]) if opts[:parent_attr]
|
198
229
|
opts[:parent] = Node.new(key: opts[:parent_locale], data: {locale: opts[:parent_locale]}) if opts[:parent_locale]
|
199
|
-
opts[:parent] ||= Node.null
|
200
230
|
end
|
201
231
|
end
|
202
232
|
end
|
@@ -1,124 +1,145 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
module I18n::Tasks
|
3
|
-
|
4
|
-
|
2
|
+
module I18n::Tasks
|
3
|
+
module Data::Tree
|
4
|
+
# Any Enumerable that yields nodes can mix in this module
|
5
|
+
module Traversal
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
def nodes(&block)
|
8
|
+
depth_first(&block)
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def leaves(&visitor)
|
12
|
+
return to_enum(:leaves) unless visitor
|
13
|
+
nodes do |node|
|
14
|
+
visitor.yield(node) if node.leaf?
|
15
|
+
end
|
16
|
+
self
|
14
17
|
end
|
15
|
-
self
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
def levels(&block)
|
20
|
+
return to_enum(:levels) unless block
|
21
|
+
nodes = to_nodes
|
22
|
+
unless nodes.empty?
|
23
|
+
block.yield nodes
|
24
|
+
Nodes.new(nodes.children).levels(&block)
|
25
|
+
end
|
26
|
+
self
|
25
27
|
end
|
26
|
-
self
|
27
|
-
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
def breadth_first(&visitor)
|
30
|
+
return to_enum(:breadth_first) unless visitor
|
31
|
+
levels do |nodes|
|
32
|
+
nodes.each { |node| visitor.yield(node) }
|
33
|
+
end
|
34
|
+
self
|
33
35
|
end
|
34
|
-
self
|
35
|
-
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
def depth_first(&visitor)
|
38
|
+
return to_enum(:depth_first) unless visitor
|
39
|
+
each { |node|
|
40
|
+
visitor.yield node
|
41
|
+
node.children.each do |child|
|
42
|
+
child.depth_first(&visitor)
|
43
|
+
end if node.children?
|
44
|
+
}
|
45
|
+
self
|
46
|
+
end
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
48
|
+
# @option root include root in full key
|
49
|
+
def keys(key_opts = {}, &visitor)
|
50
|
+
key_opts[:root] = false unless key_opts.key?(:root)
|
51
|
+
return to_enum(:keys, key_opts) unless visitor
|
52
|
+
leaves { |node| visitor.yield(node.full_key(key_opts), node) }
|
53
|
+
self
|
54
|
+
end
|
55
55
|
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
57
|
+
def key_names(opts = {})
|
58
|
+
opts[:root] = false unless opts.key?(:root)
|
59
|
+
keys(opts).map { |key, _node| key }
|
60
|
+
end
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
def key_values(opts = {})
|
63
|
+
opts[:root] = false unless opts.key?(:root)
|
64
|
+
keys(opts).map { |key, node| [key, node.value] }
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
def root_key_values(sort = false)
|
68
|
+
result = keys(root: false).map { |key, node| [node.root.key, key, node.value] }
|
69
|
+
result.sort! { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] } if sort
|
70
|
+
result
|
71
|
+
end
|
72
72
|
|
73
|
-
|
73
|
+
#-- modify / derive
|
74
|
+
|
75
|
+
# @return Siblings
|
76
|
+
def select_nodes(&block)
|
77
|
+
tree = Siblings.new
|
78
|
+
each do |node|
|
79
|
+
if block.yield(node)
|
80
|
+
tree.append! node.derive(
|
81
|
+
parent: tree.parent,
|
82
|
+
children: (node.children.select_nodes(&block).to_a if node.children)
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
tree
|
87
|
+
end
|
74
88
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
89
|
+
# @return Siblings
|
90
|
+
def select_keys(opts = {}, &block)
|
91
|
+
root = opts.key?(:root) ? opts[:root] : false
|
92
|
+
ok = {}
|
93
|
+
keys(root: root) do |full_key, node|
|
94
|
+
if block.yield(full_key, node)
|
95
|
+
node.walk_to_root { |p|
|
96
|
+
break if ok[p]
|
97
|
+
ok[p] = true
|
98
|
+
}
|
99
|
+
end
|
84
100
|
end
|
101
|
+
select_nodes { |node|
|
102
|
+
ok[node]
|
103
|
+
}
|
85
104
|
end
|
86
|
-
tree
|
87
|
-
end
|
88
105
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
node.walk_to_root { |p|
|
96
|
-
break if ok[p]
|
97
|
-
ok[p] = true
|
106
|
+
# @return Siblings
|
107
|
+
def intersect_keys(other_tree, key_opts = {}, &block)
|
108
|
+
if block
|
109
|
+
select_keys(key_opts) { |key, node|
|
110
|
+
other_node = other_tree[key]
|
111
|
+
other_node && block.call(key, node, other_node)
|
98
112
|
}
|
113
|
+
else
|
114
|
+
select_keys(key_opts) { |key, node| other_tree[key] }
|
99
115
|
end
|
100
116
|
end
|
101
|
-
select_nodes { |node|
|
102
|
-
ok[node]
|
103
|
-
}
|
104
|
-
end
|
105
117
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
select_keys(key_opts) { |key, node|
|
111
|
-
other_node = other_tree[key]
|
112
|
-
other_node && block.call(key, node, other_node)
|
113
|
-
}
|
114
|
-
else
|
115
|
-
select_keys(key_opts) { |key, node| other_tree[key] }
|
118
|
+
def grep_keys(match, opts = {})
|
119
|
+
select_keys(opts) do |full_key, _node|
|
120
|
+
match === full_key
|
121
|
+
end
|
116
122
|
end
|
117
|
-
end
|
118
123
|
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
def set_each_value!(val_pattern, key_pattern = nil, &value_proc)
|
125
|
+
value_proc ||= proc { |node|
|
126
|
+
node_value = node.value
|
127
|
+
human_key = node.key.to_s.humanize
|
128
|
+
StringInterpolation.interpolate_soft(
|
129
|
+
val_pattern,
|
130
|
+
value: node_value,
|
131
|
+
human_key: human_key,
|
132
|
+
value_or_human_key: node_value.presence || human_key
|
133
|
+
)
|
134
|
+
}
|
135
|
+
if key_pattern.present?
|
136
|
+
pattern_re = I18n::Tasks::KeyPatternMatching.compile_key_pattern(key_pattern)
|
137
|
+
end
|
138
|
+
keys.each do |key, node|
|
139
|
+
next if pattern_re && key !~ pattern_re
|
140
|
+
node.value = value_proc.call(node)
|
141
|
+
end
|
142
|
+
self
|
122
143
|
end
|
123
144
|
end
|
124
145
|
end
|
@@ -1,41 +1,46 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
require 'easy_translate'
|
3
|
+
require 'i18n/tasks/html_keys'
|
3
4
|
|
4
5
|
module I18n::Tasks
|
5
6
|
module GoogleTranslation
|
7
|
+
def google_translate_forest(forest, from, to)
|
8
|
+
list = forest.key_values(root: true)
|
9
|
+
values = google_translate_list(list, to: to, from: from).map(&:last)
|
10
|
+
Data::Tree::Siblings.from_flat_pairs list.map(&:first).zip(values)
|
11
|
+
end
|
12
|
+
|
6
13
|
# @param [Array] list of [key, value] pairs
|
7
|
-
def
|
14
|
+
def google_translate_list(list, opts)
|
8
15
|
return [] if list.empty?
|
9
|
-
opts
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
list.each_with_index { |k_v, i| key_idx[k_v[0]] = i }
|
19
|
-
list.group_by { |k_v|
|
20
|
-
!!(k_v[0] =~ /[.\-_]html\z/.freeze)
|
21
|
-
}.map do |html, slice|
|
22
|
-
t_opts = opts.merge(html ? {html: true} : {format: 'text'})
|
23
|
-
fetch_google_translations slice, t_opts
|
24
|
-
end.reduce(:+).tap { |l|
|
25
|
-
l.sort! { |a, b| key_idx[a[0]] <=> key_idx[b[0]] }
|
26
|
-
}
|
16
|
+
opts = opts.dup
|
17
|
+
opts[:key] ||= translation_config[:api_key]
|
18
|
+
validate_google_translate_api_key! opts[:key]
|
19
|
+
key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx[k] = i; idx }
|
20
|
+
result = list.group_by { |k_v| HtmlKeys.html_key? k_v[0] }.map { |is_html, list_slice|
|
21
|
+
fetch_google_translations list_slice, opts.merge(is_html ? {html: true} : {format: 'text'})
|
22
|
+
}.reduce(:+) || []
|
23
|
+
result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
|
24
|
+
result
|
27
25
|
end
|
28
26
|
|
29
27
|
def fetch_google_translations(list, opts)
|
30
28
|
from_values(list, EasyTranslate.translate(to_values(list), opts)).tap do |result|
|
31
29
|
if result.blank?
|
32
|
-
raise CommandError.new('
|
30
|
+
raise CommandError.new(I18n.t('i18n_tasks.google_translate.errors.no_results'))
|
33
31
|
end
|
34
32
|
end
|
35
33
|
end
|
36
34
|
|
37
35
|
private
|
38
36
|
|
37
|
+
def validate_google_translate_api_key!(key)
|
38
|
+
if key.blank?
|
39
|
+
raise CommandError.new('Set Google API key via GOOGLE_TRANSLATE_API_KEY environment variable or translation.api_key in config/i18n-tasks.yml.
|
40
|
+
Get the key at https://code.google.com/apis/console.')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
39
44
|
def to_values(list)
|
40
45
|
list.map { |l| dump_value l[1] }.flatten
|
41
46
|
end
|
@@ -78,10 +83,5 @@ module I18n::Tasks
|
|
78
83
|
each_value = untranslated.scan(INTERPOLATION_KEY_RE).to_enum
|
79
84
|
translated.gsub(Regexp.new(UNTRANSLATABLE_STRING, Regexp::IGNORECASE)) { each_value.next }
|
80
85
|
end
|
81
|
-
|
82
|
-
def warn_missing_api_key
|
83
|
-
$stderr.puts Term::ANSIColor.red Term::ANSIColor.yellow 'Set Google API key via GOOGLE_TRANSLATE_API_KEY environmnet variable or translation.api_key in config/i18n-tasks.yml.
|
84
|
-
Get the key at https://code.google.com/apis/console.'
|
85
|
-
end
|
86
86
|
end
|
87
87
|
end
|