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.
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