i18n-tasks 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|