i18n-tasks 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +9 -0
  3. data/README.md +46 -36
  4. data/Rakefile +2 -2
  5. data/lib/i18n/tasks/base_task.rb +2 -0
  6. data/lib/i18n/tasks/commands.rb +55 -35
  7. data/lib/i18n/tasks/commands_base.rb +36 -9
  8. data/lib/i18n/tasks/configuration.rb +14 -13
  9. data/lib/i18n/tasks/console_context.rb +0 -1
  10. data/lib/i18n/tasks/data.rb +2 -2
  11. data/lib/i18n/tasks/data/adapter/json_adapter.rb +6 -2
  12. data/lib/i18n/tasks/data/file_formats.rb +19 -4
  13. data/lib/i18n/tasks/data/file_system_base.rb +1 -1
  14. data/lib/i18n/tasks/data/tree/node.rb +11 -37
  15. data/lib/i18n/tasks/data/tree/nodes.rb +6 -2
  16. data/lib/i18n/tasks/data/tree/siblings.rb +53 -29
  17. data/lib/i18n/tasks/data/tree/traversal.rb +4 -2
  18. data/lib/i18n/tasks/fill_tasks.rb +5 -2
  19. data/lib/i18n/tasks/ignore_keys.rb +1 -1
  20. data/lib/i18n/tasks/locale_pathname.rb +1 -1
  21. data/lib/i18n/tasks/logging.rb +2 -0
  22. data/lib/i18n/tasks/missing_keys.rb +74 -31
  23. data/lib/i18n/tasks/plural_keys.rb +4 -3
  24. data/lib/i18n/tasks/reports/base.rb +8 -5
  25. data/lib/i18n/tasks/reports/spreadsheet.rb +15 -3
  26. data/lib/i18n/tasks/reports/terminal.rb +62 -23
  27. data/lib/i18n/tasks/scanners/base_scanner.rb +4 -2
  28. data/lib/i18n/tasks/scanners/relative_keys.rb +21 -0
  29. data/lib/i18n/tasks/split_key.rb +39 -0
  30. data/lib/i18n/tasks/used_keys.rb +1 -1
  31. data/lib/i18n/tasks/version.rb +1 -1
  32. data/spec/fixtures/app/views/index.html.slim +2 -0
  33. data/spec/i18n_tasks_spec.rb +19 -5
  34. data/spec/locale_pathname_spec.rb +24 -0
  35. data/spec/locale_tree/siblings_spec.rb +41 -4
  36. data/spec/split_key_spec.rb +27 -0
  37. data/spec/support/i18n_tasks_output_matcher.rb +7 -13
  38. data/spec/support/trees.rb +4 -0
  39. data/templates/config/i18n-tasks.yml +82 -0
  40. data/templates/rspec/i18n_spec.rb +18 -0
  41. metadata +10 -3
  42. data/lib/i18n/tasks/relative_keys.rb +0 -19
@@ -69,7 +69,6 @@ module I18n::Tasks
69
69
  t(key, locale)
70
70
  key_value?(key, locale)
71
71
  depluralize_key(key, locale) # convert 'hat.one' to 'hat'
72
- absolutize_key(key, path) # '.title' to 'users.index.title'
73
72
  TEXT
74
73
  end
75
74
  end
@@ -21,8 +21,8 @@ module I18n::Tasks
21
21
  data.t(key, locale)
22
22
  end
23
23
 
24
- def tree(locale)
25
- data[locale][locale].children
24
+ def tree(sel)
25
+ data[split_key(sel, 2).first][sel].try(:children)
26
26
  end
27
27
 
28
28
  def node(key, locale = base_locale)
@@ -9,14 +9,18 @@ module I18n::Tasks
9
9
 
10
10
  # @return [Hash] locale tree
11
11
  def parse(str, opts)
12
- JSON.parse(str, opts || {})
12
+ JSON.parse(str, parse_opts(opts))
13
13
  end
14
14
 
15
15
  # @return [String]
16
16
  def dump(tree, opts)
17
- JSON.generate(tree, opts || {})
17
+ JSON.generate(tree, parse_opts(opts))
18
18
  end
19
19
 
20
+ private
21
+ def parse_opts(opts)
22
+ opts.try(:symbolize_keys) || {}
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -13,6 +13,16 @@ module I18n
13
13
  self.class.adapter_for(path)
14
14
  end
15
15
 
16
+ def adapter_by_name(path)
17
+ self.class.adapter_by_name(path)
18
+ end
19
+
20
+ def adapter_dump(tree, adapter_info)
21
+ adapter_name, adapter_pattern, adapter = adapter_info
22
+ adapter_options = (config[adapter_name] || {})[:write]
23
+ adapter.dump(tree, adapter_options)
24
+ end
25
+
16
26
  protected
17
27
 
18
28
  def load_file(path)
@@ -24,9 +34,7 @@ module I18n
24
34
  def write_tree(path, tree)
25
35
  ::FileUtils.mkpath(File.dirname path)
26
36
  ::File.open(path, 'w') { |f|
27
- adapter_name, adapter_pattern, adapter = adapter_for(path)
28
- adapter_options = (config[adapter_name] || {})[:write]
29
- f.write(adapter.dump(tree.to_hash, adapter_options))
37
+ f.write(adapter_dump(tree.to_hash, adapter_for(path)))
30
38
  }
31
39
  end
32
40
 
@@ -38,10 +46,17 @@ module I18n
38
46
  end
39
47
 
40
48
  def adapter_for(path)
41
- @fn_patterns.detect { |(name, pattern, adapter)|
49
+ @fn_patterns.detect { |(_name, pattern, _adapter)|
42
50
  ::File.fnmatch(pattern, path)
43
51
  } or raise CommandError.new("Adapter not found for #{path}. Registered adapters: #{@fn_patterns.inspect}")
44
52
  end
53
+
54
+ def adapter_by_name(name)
55
+ name = name.to_s
56
+ @fn_patterns.detect { |(adapter_name, _pattern, _adapter)|
57
+ adapter_name.to_s == name
58
+ } or raise CommandError.new("Adapter with name #{name.inspect} not found. Registered adapters: #{@fn_patterns.inspect}")
59
+ end
45
60
  end
46
61
  end
47
62
  end
@@ -111,7 +111,7 @@ module I18n::Tasks
111
111
  [path.freeze, load_file(path) || {}]
112
112
  end.map do |path, data|
113
113
  Data::Tree::Siblings.from_nested_hash(data).tap do |s|
114
- s.leaves { |x| x.data[:path] = path }
114
+ s.leaves { |x| x.data.update(path: path, locale: locale) }
115
115
  end
116
116
  end.reduce(:merge!) || Tree::Siblings.null
117
117
  end
@@ -20,28 +20,19 @@ module I18n::Tasks::Data::Tree
20
20
  end
21
21
 
22
22
  def derive(new_attr = {})
23
- node = self.class.new(attributes.merge(new_attr).except(:children))
24
- node.children = new_attr[:children] || @children.try(:derive, parent: node)
25
- node
26
- end
27
-
28
- def key=(value)
29
- value = value.try(:to_s)
30
- if @key != value
31
- dirty!
32
- parent.try(:children).try(:key_renamed, value, @key)
33
- @key = value
34
- end
23
+ self.class.new(attributes.merge(new_attr))
35
24
  end
36
25
 
37
26
  def children=(children)
27
+ @children = case children
28
+ when Siblings
29
+ children.parent == self ? children : children.derive(parent: self)
30
+ when NilClass
31
+ nil
32
+ else
33
+ Siblings.new(nodes: children, parent: self)
34
+ end
38
35
  dirty!
39
- if Siblings === children || children.nil?
40
- @children = children
41
- @children.parent = self if @children
42
- else
43
- @children = Siblings.new(nodes: children, parent: self)
44
- end
45
36
  end
46
37
 
47
38
  def each(&block)
@@ -88,19 +79,6 @@ module I18n::Tasks::Data::Tree
88
79
  @data.present?
89
80
  end
90
81
 
91
- # do not use directly. use parent.append(node) instead
92
- def parent=(value)
93
- return if @parent == value
94
- @parent.children.remove!(self) if @parent.try(:children) && @parent.children != value.children
95
- @parent = value
96
- dirty!
97
- @parent
98
- end
99
-
100
- def siblings
101
- parent.children
102
- end
103
-
104
82
  def get(key)
105
83
  children.get(key)
106
84
  end
@@ -151,11 +129,7 @@ module I18n::Tasks::Data::Tree
151
129
  end
152
130
 
153
131
  def to_siblings
154
- if parent
155
- parent.children
156
- else
157
- Siblings.new(nodes: [self])
158
- end
132
+ parent.try(:children) || Siblings.new(nodes: [self])
159
133
  end
160
134
 
161
135
  def to_hash
@@ -201,7 +175,7 @@ module I18n::Tasks::Data::Tree
201
175
  def from_key_value(key, value)
202
176
  Node.new(key: key.try(:to_s)).tap do |node|
203
177
  if value.is_a?(Hash)
204
- node.children = Siblings.from_nested_hash(value, parent: node)
178
+ node.children = Siblings.from_nested_hash(value)
205
179
  else
206
180
  node.value = value
207
181
  end
@@ -23,8 +23,12 @@ module I18n::Tasks::Data::Tree
23
23
  end
24
24
 
25
25
  def derive(new_attr = {})
26
- attr = attributes.merge(new_attr)
27
- attr[:nodes] ||= @list.map(&:derive)
26
+ attr = attributes.except(:nodes, :parent).merge(new_attr)
27
+ if self.parent
28
+ new_attr[:parent] ||= Node.null
29
+ end
30
+ node_attr = new_attr.slice(:parent)
31
+ attr[:nodes] ||= @list.map { |node| node.derive(node_attr) }
28
32
  self.class.new(attr)
29
33
  end
30
34
 
@@ -11,29 +11,31 @@ module I18n::Tasks::Data::Tree
11
11
 
12
12
  def initialize(opts = {})
13
13
  super(nodes: opts[:nodes])
14
- @key_to_node = siblings.inject({}) { |h, node| h[node.key] = node; h }
15
- @parent = first.try(:parent)
16
- self.parent = opts[:parent] || @parent || Node.null
14
+ @parent = opts[:parent] || first.try(:parent)
15
+ @list.map! { |node| node.parent == @parent ? node : node.derive(parent: @parent) }
16
+ @key_to_node = @list.inject({}) { |h, node| h[node.key] = node; h }
17
17
  end
18
18
 
19
19
  def attributes
20
20
  super.merge(parent: @parent)
21
21
  end
22
22
 
23
- def parent=(node)
24
- return if @parent == node
25
- each { |root| root.parent = node }
26
- @parent = node
23
+ def rename_key(key, new_key)
24
+ node = key_to_node.delete(key)
25
+ replace_node! node, node.derive(key: new_key)
26
+ self
27
27
  end
28
28
 
29
- def siblings(&block)
30
- each(&block)
31
- self
29
+ def replace_node!(node, new_node)
30
+ @list[@list.index(node)] = new_node
31
+ key_to_node[new_node.key] = new_node
32
32
  end
33
33
 
34
+ include SplitKey
35
+
34
36
  # @return [Node] by full key
35
37
  def get(full_key)
36
- first_key, rest = full_key.to_s.split('.', 2)
38
+ first_key, rest = split_key(full_key, 2)
37
39
  node = key_to_node[first_key]
38
40
  if rest && node
39
41
  node = node.children.try(:get, rest)
@@ -45,20 +47,25 @@ module I18n::Tasks::Data::Tree
45
47
 
46
48
  # add or replace node by full key
47
49
  def set(full_key, node)
48
- key_part, rest = full_key.split('.', 2)
49
- child = key_to_node[key_part]
50
+ raise 'value should be a I18n::Tasks::Data::Tree::Node' unless node.is_a?(Node)
51
+ key_part, rest = split_key(full_key, 2)
52
+ child = key_to_node[key_part]
53
+
50
54
  if rest
51
55
  unless child
52
- child = Node.new(key: key_part)
56
+ child = Node.new(key: key_part, parent: parent, children: [])
53
57
  append! child
54
58
  end
55
- child.children ||= []
59
+ unless child.children
60
+ warn_add_children_to_leaf child
61
+ child.children = []
62
+ end
56
63
  child.children.set rest, node
57
- dirty!
58
64
  else
59
65
  remove! child if child
60
66
  append! node
61
67
  end
68
+ dirty!
62
69
  node
63
70
  end
64
71
 
@@ -74,12 +81,11 @@ module I18n::Tasks::Data::Tree
74
81
  end
75
82
 
76
83
  def append!(nodes)
77
- nodes.each do |node|
78
- raise "node '#{node.full_key}' already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
79
- key_to_node[node.key] = node
80
- node.parent = parent
84
+ nodes = nodes.map do |node|
85
+ raise "already has a child with key '#{node.key}'" if key_to_node.key?(node.key)
86
+ key_to_node[node.key] = (node.parent == parent ? node : node.derive(parent: parent))
81
87
  end
82
- super
88
+ super(nodes)
83
89
  self
84
90
  end
85
91
 
@@ -95,7 +101,14 @@ module I18n::Tasks::Data::Tree
95
101
  next if our == node
96
102
  our.value = node.value if node.leaf?
97
103
  our.data.merge!(node.data) if node.data?
98
- our.children.merge!(node.children) if node.children?
104
+ if node.children?
105
+ if our.children
106
+ our.children.merge!(node.children)
107
+ else
108
+ warn_add_children_to_leaf our
109
+ our.children = node.children
110
+ end
111
+ end
99
112
  else
100
113
  key_to_node[node.key] = node.derive(parent: parent)
101
114
  end
@@ -109,12 +122,22 @@ module I18n::Tasks::Data::Tree
109
122
  derive.merge!(nodes)
110
123
  end
111
124
 
112
- def key_renamed(new_name, old_name)
113
- node = key_to_node.delete old_name
114
- key_to_node[new_name] = node
125
+ def set_root_key(new_key, data = nil)
126
+ return self if empty?
127
+ rename_key first.key, new_key
128
+ leaves { |node| node.data.merge! data } if data
129
+ self
130
+ end
131
+
132
+ private
133
+
134
+ def warn_add_children_to_leaf(node)
135
+ ::I18n::Tasks::Logging.log_warn "'#{node.full_key}' was a leaf, now has children (value <- scope conflict)"
115
136
  end
116
137
 
117
138
  class << self
139
+ include SplitKey
140
+
118
141
  def null
119
142
  new
120
143
  end
@@ -124,14 +147,15 @@ module I18n::Tasks::Data::Tree
124
147
  parse_parent_opt!(opts)
125
148
  forest = Siblings.new(opts)
126
149
  block.call(forest) if block
127
- forest.parent.children = forest
150
+ # forest.parent.children = forest
151
+ forest
128
152
  end
129
153
 
130
154
  def from_key_attr(key_attrs, opts = {}, &block)
131
155
  build_forest(opts) { |forest|
132
156
  key_attrs.each { |(full_key, attr)|
133
157
  raise "Invalid key #{full_key.inspect}" if full_key.end_with?('.')
134
- node = Node.new(attr.merge(key: full_key.split('.').last))
158
+ node = Node.new(attr.merge(key: split_key(full_key).last))
135
159
  block.call(full_key, node) if block
136
160
  forest[full_key] = node
137
161
  }
@@ -141,7 +165,7 @@ module I18n::Tasks::Data::Tree
141
165
  def from_key_names(keys, opts = {}, &block)
142
166
  build_forest(opts) { |forest|
143
167
  keys.each { |full_key|
144
- node = Node.new(key: full_key.split('.').last)
168
+ node = Node.new(key: split_key(full_key).last)
145
169
  block.call(full_key, node) if block
146
170
  forest[full_key] = node
147
171
  }
@@ -162,7 +186,7 @@ module I18n::Tasks::Data::Tree
162
186
  def from_flat_pairs(pairs)
163
187
  Siblings.new.tap do |siblings|
164
188
  pairs.each { |full_key, value|
165
- siblings[full_key] = Node.new(key: full_key.split('.')[-1], value: value)
189
+ siblings[full_key] = Node.new(key: split_key(full_key).last, value: value)
166
190
  }
167
191
  end
168
192
  end
@@ -64,8 +64,10 @@ module I18n::Tasks::Data::Tree
64
64
  keys(opts).map { |key, node| [key, node.value] }
65
65
  end
66
66
 
67
- def root_key_values
68
- keys(root: false).map { |key, node| [node.root.key, key, node.value]}
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
69
71
  end
70
72
 
71
73
  #-- modify / derive
@@ -5,9 +5,12 @@ module I18n::Tasks
5
5
  value = opts[:value] || ''
6
6
  base = opts[:base_locale] || base_locale
7
7
  locales_for_update(opts).each do |locale|
8
- m = missing_tree(locale, base).keys { |key, node|
8
+ m = missing_keys(locales: [locale], base_locale: base).keys { |key, node|
9
9
  node.value = value.respond_to?(:call) ? value.call(key, locale, node) : value
10
- node.data[:path] = LocalePathname.replace_locale(node.data[:path], base, locale) if node.data.key?(:path)
10
+ if node.data.key?(:path)
11
+ # set path hint for the router
12
+ node.data.update path: LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale), locale: locale
13
+ end
11
14
  }
12
15
  data[locale] = data[locale].merge! m
13
16
  end
@@ -7,7 +7,7 @@ module I18n::Tasks::IgnoreKeys
7
7
  key =~ ignore_pattern(ignore_type, locale)
8
8
  end
9
9
 
10
- # @param type [:missing, :unused, :eq_base] type
10
+ # @param type [nil, :missing, :unused, :eq_base] type
11
11
  # @param locale [String] only when type is :eq_base
12
12
  # @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
13
13
  def ignore_pattern(type, locale = nil)
@@ -3,7 +3,7 @@ module I18n::Tasks
3
3
  extend self
4
4
 
5
5
  def replace_locale(path, from, to)
6
- path.try :sub, /(^|[\/.])#{from}(?=\.)/, "\\1#{to}"
6
+ path.try :sub, /(?<=^|[\/.])#{from}(?=\.)/, "#{to}"
7
7
  end
8
8
  end
9
9
  end
@@ -1,5 +1,7 @@
1
1
  # coding: utf-8
2
2
  module I18n::Tasks::Logging
3
+ extend self
4
+
3
5
  def warn_deprecated(message)
4
6
  log_stderr Term::ANSIColor.yellow Term::ANSIColor.bold "i18n-tasks: [DEPRECATED] #{message}"
5
7
  end
@@ -2,55 +2,98 @@
2
2
  module I18n::Tasks
3
3
  module MissingKeys
4
4
  def missing_keys_types
5
- @missing_keys_types ||= [:missing_from_base, :eq_base, :missing_from_locale]
5
+ @missing_keys_types ||= [:used, :diff]
6
6
  end
7
7
 
8
- # @param [:missing_from_base, :missing_from_locale, :eq_base] type (default nil)
8
+ # @param [:missing_used, :missing_diff] type (default nil)
9
9
  # @return [Siblings]
10
10
  def missing_keys(opts = {})
11
11
  locales = Array(opts[:locales]).presence || self.locales
12
- types = Array(opts[:type] || opts[:types].presence || missing_keys_types)
12
+ types = (Array(opts[:types]).presence || missing_keys_types).map(&:to_s)
13
+ validate_missing_types! types
14
+ base = opts[:base_locale] || base_locale
15
+ tree = Data::Tree::Siblings.new
13
16
 
14
- types.map { |type|
15
- case type.to_s
16
- when 'missing_from_base'
17
- missing_tree(base_locale) if locales.include?(base_locale)
18
- when 'missing_from_locale'
19
- non_base_locales(locales).map { |locale| missing_tree(locale) }.reduce(:merge!)
20
- when 'eq_base'
21
- non_base_locales(locales).map { |locale| eq_base_tree(locale) }.reduce(:merge!)
22
- end
23
- }.compact.reduce(:merge!)
17
+ types.each do |type|
18
+ tree.merge! send(:"missing_#{type}_forest", locales, base)
19
+ end
20
+ tree
21
+ end
22
+
23
+ def eq_base_keys(opts = {})
24
+ locales = Array(opts[:locales]).presence || self.locales
25
+ (locales - [base_locale]).inject(Data::Tree::Siblings.new) { |tree, locale|
26
+ tree.merge! equal_values_tree(locale, base_locale)
27
+ }
28
+ end
29
+
30
+ def missing_diff_forest(locales, base = base_locale)
31
+ tree = Data::Tree::Siblings.new
32
+ # present in base but not locale
33
+ (locales - [base]).each { |locale|
34
+ tree.merge! missing_diff_tree(locale, base)
35
+ }
36
+ if locales.include?(base)
37
+ # present in locale but not base
38
+ (self.locales - [base]).each { |locale|
39
+ tree.merge! missing_diff_tree(base, locale).set_root_key(base)
40
+ }
41
+ end
42
+ tree
43
+ end
44
+
45
+ def missing_used_forest(locales, base = base_locale)
46
+ if locales.include?(base)
47
+ missing_used_tree(base)
48
+ else
49
+ Data::Tree::Siblings.new
50
+ end
24
51
  end
25
52
 
26
- def missing_tree(locale, compared_to = base_locale)
53
+ def missing_tree(locale, compared_to)
27
54
  if locale == compared_to
28
- # keys used, but not present in locale
29
- set_locale_tree_type used_tree.select_keys { |key, node|
30
- !(key_expression?(key) || key_value?(key, locale) || ignore_key?(key, :missing))
31
- }, locale, :missing_from_base
55
+ missing_used_tree locale
32
56
  else
33
- # keys present in compared_to, but not in locale
34
- collapse_plural_nodes! set_locale_tree_type data[compared_to].select_keys { |key, node|
35
- !key_value?(key, locale) && !ignore_key?(key, :missing)
36
- }, locale, :missing_from_locale
57
+ missing_diff_tree locale, compared_to
37
58
  end
38
59
  end
39
60
 
40
- def eq_base_tree(locale, compare_to = base_locale)
61
+ # keys present in compared_to, but not in locale
62
+ def missing_diff_tree(locale, compared_to = base_locale)
63
+ data[compared_to].select_keys { |key, _node|
64
+ locale_key_missing?(locale, key)
65
+ }.set_root_key(locale, type: :missing_diff).tap { |t| collapse_plural_nodes!(t) }
66
+ end
67
+
68
+ # keys used in the code missing translations in locale
69
+ def missing_used_tree(locale)
70
+ used_tree.select_keys { |key, _node|
71
+ !key_expression?(key) && locale_key_missing?(locale, key)
72
+ }.set_root_key(locale, type: :missing_used)
73
+ end
74
+
75
+ def equal_values_tree(locale, compare_to = base_locale)
41
76
  base = data[compare_to].first.children
42
- set_locale_tree_type data[locale].select_keys(root: false) { |key, node|
77
+ data[locale].select_keys(root: false) { |key, node|
43
78
  other_node = base[key]
44
79
  other_node && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
45
- }, locale, :eq_base
80
+ }.set_root_key(locale, type: :eq_base)
46
81
  end
47
82
 
48
- def set_locale_tree_type(tree, locale, type)
49
- tree.siblings { |root|
50
- root.key = locale
51
- }.leaves { |node|
52
- node.data[:type] = type
53
- }
83
+ def locale_key_missing?(locale, key)
84
+ !key_value?(key, locale) && !ignore_key?(key, :missing)
85
+ end
86
+
87
+ private
88
+
89
+ def validate_missing_types!(types)
90
+ valid_types = missing_keys_types.map(&:to_s)
91
+ types = types.map(&:to_s)
92
+ invalid_types = types - valid_types
93
+ if invalid_types.present?
94
+ raise CommandError.new("Unknown types: #{invalid_types * ', '}. Valid types are: #{valid_types * ', '}.")
95
+ end
96
+ true
54
97
  end
55
98
  end
56
99
  end