i18n-tasks 0.5.4 → 0.6.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 (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