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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile +2 -1
  4. data/README.md +80 -78
  5. data/bin/i18n-tasks +24 -30
  6. data/config/i18n-tasks.yml +87 -0
  7. data/config/locales/en.yml +95 -0
  8. data/i18n-tasks.gemspec +1 -0
  9. data/lib/i18n/tasks.rb +10 -0
  10. data/lib/i18n/tasks/base_task.rb +6 -2
  11. data/lib/i18n/tasks/command/collection.rb +18 -0
  12. data/lib/i18n/tasks/command/commander.rb +72 -0
  13. data/lib/i18n/tasks/command/commands/data.rb +73 -0
  14. data/lib/i18n/tasks/command/commands/eq_base.rb +20 -0
  15. data/lib/i18n/tasks/command/commands/health.rb +26 -0
  16. data/lib/i18n/tasks/command/commands/meta.rb +35 -0
  17. data/lib/i18n/tasks/command/commands/missing.rb +73 -0
  18. data/lib/i18n/tasks/command/commands/tree.rb +92 -0
  19. data/lib/i18n/tasks/command/commands/usages.rb +70 -0
  20. data/lib/i18n/tasks/command/commands/xlsx.rb +27 -0
  21. data/lib/i18n/tasks/command/dsl.rb +27 -0
  22. data/lib/i18n/tasks/command/dsl/cmd.rb +19 -0
  23. data/lib/i18n/tasks/command/dsl/cmd_opt.rb +19 -0
  24. data/lib/i18n/tasks/command/dsl/enum_opt.rb +26 -0
  25. data/lib/i18n/tasks/command/options/common.rb +48 -0
  26. data/lib/i18n/tasks/command/options/enum_opt.rb +44 -0
  27. data/lib/i18n/tasks/command/options/list_opt.rb +11 -0
  28. data/lib/i18n/tasks/command/options/locales.rb +47 -0
  29. data/lib/i18n/tasks/command/options/trees.rb +101 -0
  30. data/lib/i18n/tasks/command_error.rb +3 -0
  31. data/lib/i18n/tasks/commands.rb +22 -169
  32. data/lib/i18n/tasks/configuration.rb +1 -16
  33. data/lib/i18n/tasks/console_context.rb +1 -1
  34. data/lib/i18n/tasks/data.rb +13 -8
  35. data/lib/i18n/tasks/data/file_formats.rb +29 -18
  36. data/lib/i18n/tasks/data/file_system_base.rb +35 -4
  37. data/lib/i18n/tasks/data/router/conservative_router.rb +18 -11
  38. data/lib/i18n/tasks/data/tree/node.rb +5 -15
  39. data/lib/i18n/tasks/data/tree/nodes.rb +0 -3
  40. data/lib/i18n/tasks/data/tree/siblings.rb +32 -2
  41. data/lib/i18n/tasks/data/tree/traversal.rb +117 -96
  42. data/lib/i18n/tasks/google_translation.rb +25 -25
  43. data/lib/i18n/tasks/html_keys.rb +10 -0
  44. data/lib/i18n/tasks/key_pattern_matching.rb +1 -0
  45. data/lib/i18n/tasks/locale_list.rb +19 -0
  46. data/lib/i18n/tasks/missing_keys.rb +32 -33
  47. data/lib/i18n/tasks/plural_keys.rb +1 -1
  48. data/lib/i18n/tasks/reports/base.rb +4 -9
  49. data/lib/i18n/tasks/reports/spreadsheet.rb +5 -5
  50. data/lib/i18n/tasks/reports/terminal.rb +62 -38
  51. data/lib/i18n/tasks/scanners/base_scanner.rb +5 -4
  52. data/lib/i18n/tasks/slop_command.rb +27 -0
  53. data/lib/i18n/tasks/stats.rb +20 -0
  54. data/lib/i18n/tasks/string_interpolation.rb +14 -0
  55. data/lib/i18n/tasks/unused_keys.rb +0 -10
  56. data/lib/i18n/tasks/version.rb +1 -1
  57. data/spec/commands/data_commands_spec.rb +38 -0
  58. data/spec/commands/tree_commands_spec.rb +68 -0
  59. data/spec/fixtures/app/views/index.html.slim +1 -0
  60. data/spec/google_translate_spec.rb +5 -3
  61. data/spec/i18n_spec.rb +18 -0
  62. data/spec/i18n_tasks_spec.rb +8 -8
  63. data/spec/spec_helper.rb +3 -3
  64. data/spec/support/test_codebase.rb +4 -1
  65. data/spec/used_keys_spec.rb +7 -7
  66. data/templates/config/i18n-tasks.yml +2 -2
  67. metadata +48 -4
  68. data/lib/i18n/tasks/commands_base.rb +0 -107
  69. 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 && !parent.null?
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 unless self.null?
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 null?
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 null?
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 set_root_key(new_key, data = nil)
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::Data::Tree
3
- # Any Enumerable that yields nodes can mix in this module
4
- module Traversal
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
- def nodes(&block)
7
- depth_first(&block)
8
- end
7
+ def nodes(&block)
8
+ depth_first(&block)
9
+ end
9
10
 
10
- def leaves(&visitor)
11
- return to_enum(:leaves) unless visitor
12
- nodes do |node|
13
- visitor.yield(node) if node.leaf?
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
- def levels(&block)
19
- return to_enum(:levels) unless block
20
- nodes = to_nodes
21
- non_null = nodes.reject(&:null?)
22
- unless non_null.empty?
23
- block.yield non_null
24
- Nodes.new(nodes.children).levels(&block)
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
- def breadth_first(&visitor)
30
- return to_enum(:breadth_first) unless visitor
31
- levels do |nodes|
32
- nodes.each { |node| visitor.yield(node) unless node.null? }
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
- def depth_first(&visitor)
38
- return to_enum(:depth_first) unless visitor
39
- each { |node|
40
- visitor.yield node unless node.null?
41
- node.children.each do |child|
42
- child.depth_first(&visitor)
43
- end if node.children?
44
- }
45
- self
46
- end
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
- # @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
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
- def key_names(opts = {})
58
- opts[:root] = false unless opts.key?(:root)
59
- keys(opts).map { |key, _node| key }
60
- end
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
- def key_values(opts = {})
63
- opts[:root] = false unless opts.key?(:root)
64
- keys(opts).map { |key, node| [key, node.value] }
65
- end
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
- 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
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
- #-- modify / derive
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
- # @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
- )
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
- # @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
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
- # @return Siblings
108
- def intersect_keys(other_tree, key_opts = {}, &block)
109
- if block
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
- def grep_keys(match, opts = {})
120
- select_keys(opts) do |full_key, _node|
121
- match === full_key
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 google_translate(list, opts)
14
+ def google_translate_list(list, opts)
8
15
  return [] if list.empty?
9
- opts = opts.dup
10
- if !opts[:key] && (key = translation_config[:api_key]).present?
11
- opts[:key] = key
12
- end
13
- if opts[:key].blank?
14
- warn_missing_api_key
15
- return []
16
- end
17
- key_idx = {}
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('Google Translate returned no results. Make sure billing information is set at https://code.google.com/apis/console.')
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