i18n-tasks 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -4
  3. data/CHANGES.md +7 -0
  4. data/README.md +10 -14
  5. data/i18n-tasks.gemspec +1 -1
  6. data/lib/i18n/tasks.rb +0 -2
  7. data/lib/i18n/tasks/base_task.rb +4 -2
  8. data/lib/i18n/tasks/commands.rb +14 -14
  9. data/lib/i18n/tasks/configuration.rb +10 -2
  10. data/lib/i18n/tasks/console_context.rb +73 -0
  11. data/lib/i18n/tasks/data.rb +0 -47
  12. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +6 -1
  13. data/lib/i18n/tasks/data/file_system_base.rb +1 -1
  14. data/lib/i18n/tasks/data/router/conservative_router.rb +5 -5
  15. data/lib/i18n/tasks/data/router/pattern_router.rb +2 -2
  16. data/lib/i18n/tasks/data/tree/node.rb +47 -36
  17. data/lib/i18n/tasks/data/tree/nodes.rb +0 -4
  18. data/lib/i18n/tasks/data/tree/siblings.rb +54 -9
  19. data/lib/i18n/tasks/data/tree/traversal.rb +62 -23
  20. data/lib/i18n/tasks/fill_tasks.rb +29 -21
  21. data/lib/i18n/tasks/ignore_keys.rb +1 -1
  22. data/lib/i18n/tasks/key_pattern_matching.rb +17 -0
  23. data/lib/i18n/tasks/missing_keys.rb +39 -44
  24. data/lib/i18n/tasks/plural_keys.rb +14 -1
  25. data/lib/i18n/tasks/reports/base.rb +28 -8
  26. data/lib/i18n/tasks/reports/spreadsheet.rb +9 -8
  27. data/lib/i18n/tasks/reports/terminal.rb +33 -29
  28. data/lib/i18n/tasks/scanners/base_scanner.rb +22 -14
  29. data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -1
  30. data/lib/i18n/tasks/unused_keys.rb +13 -13
  31. data/lib/i18n/tasks/used_keys.rb +39 -38
  32. data/lib/i18n/tasks/version.rb +1 -1
  33. data/spec/i18n_tasks_spec.rb +41 -40
  34. data/spec/locale_tree/siblings_spec.rb +26 -1
  35. data/spec/support/i18n_tasks_output_matcher.rb +4 -1
  36. data/spec/support/trees.rb +6 -1
  37. data/spec/used_keys_spec.rb +23 -15
  38. metadata +4 -11
  39. data/lib/i18n/tasks/file_structure.rb +0 -19
  40. data/lib/i18n/tasks/key.rb +0 -48
  41. data/lib/i18n/tasks/key/key_group.rb +0 -45
  42. data/lib/i18n/tasks/key/match_pattern.rb +0 -24
  43. data/lib/i18n/tasks/key/usages.rb +0 -12
  44. data/lib/i18n/tasks/key_group.rb +0 -68
  45. data/spec/key_group_spec.rb +0 -49
@@ -14,7 +14,7 @@ module I18n::Tasks::IgnoreKeys
14
14
  @ignore_patterns ||= HashWithIndifferentAccess.new
15
15
  @ignore_patterns[type] ||= {}
16
16
  @ignore_patterns[type][locale] ||= begin
17
- global, type_ignore = config[:ignore].presence || [], config["ignore_#{type}"].presence || []
17
+ global, type_ignore = ignore_config.presence || [], ignore_config(type).presence || []
18
18
  if type_ignore.is_a?(Array)
19
19
  patterns = global + type_ignore
20
20
  elsif type_ignore.is_a?(Hash)
@@ -29,4 +29,21 @@ module I18n::Tasks::KeyPatternMatching
29
29
  gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)').
30
30
  gsub(/\{(.*?)}/) { "(#{$1.strip.gsub /\s*,\s*/, '|'})" }
31
31
  end
32
+
33
+ def key_match_pattern(k)
34
+ @key_match_pattern ||= {}
35
+ @key_match_pattern[k] ||= begin
36
+ "#{k.gsub(KEY_INTERPOLATION_RE, '*')}#{'*' if k.end_with?('.')}"
37
+ end
38
+ end
39
+
40
+ # @return true if the key looks like an expression
41
+ KEY_INTERPOLATION_RE = /(?:\#{.*?}|\*+)/.freeze
42
+ def key_expression?(k)
43
+ @key_is_expr ||= {}
44
+ if @key_is_expr[k].nil?
45
+ @key_is_expr[k] = (k =~ KEY_INTERPOLATION_RE || k.end_with?('.'))
46
+ end
47
+ @key_is_expr[k]
48
+ end
32
49
  end
@@ -1,61 +1,56 @@
1
1
  # coding: utf-8
2
2
  module I18n::Tasks
3
3
  module MissingKeys
4
+ def missing_keys_types
5
+ @missing_keys_types ||= [:missing_from_base, :eq_base, :missing_from_locale]
6
+ end
7
+
4
8
  # @param [:missing_from_base, :missing_from_locale, :eq_base] type (default nil)
5
- # @return [KeyGroup]
9
+ # @return [Siblings]
6
10
  def missing_keys(opts = {})
7
11
  locales = Array(opts[:locales]).presence || self.locales
8
- type = opts[:type]
9
- unless type
10
- types = opts[:types].presence || missing_keys_types
11
- opts = opts.except(:types).merge(locales: locales)
12
- return types.map { |t| missing_keys(opts.merge(type: t)) }.reduce(:+)
13
- end
14
-
15
- if type.to_s == 'missing_from_base'
16
- keys = keys_missing_from_base if locales.include?(base_locale)
17
- else
18
- keys = non_base_locales(locales).map { |locale|
19
- send("keys_#{type}", locale)
20
- }.reduce(:+)
21
- end
22
- keys || KeyGroup.new([])
23
- end
12
+ types = Array(opts[:type] || opts[:types].presence || missing_keys_types)
24
13
 
25
- def missing_keys_types
26
- @missing_keys_types ||= [:missing_from_base, :eq_base, :missing_from_locale]
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!)
27
24
  end
28
25
 
29
- # @return [KeyGroup] missing keys, i.e. key that are in the code but are not in the base locale data
30
- def keys_missing_from_base
31
- @keys_missing_from_base ||= begin
32
- keys = used_keys.keys.reject { |k|
33
- key = k.key
34
- k.expr? || key_value?(key, base_locale) || ignore_key?(key, :missing)
35
- }.map(&:clone_orphan)
36
- KeyGroup.new keys, type: :missing_from_base, locale: base_locale
26
+ def missing_tree(locale, compared_to = base_locale)
27
+ 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
32
+ 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
37
37
  end
38
38
  end
39
39
 
40
- # @return [KeyGroup] keys missing (nil or blank?) in locale but present in base
41
- def keys_missing_from_locale(locale)
42
- return keys_missing_from_base if locale == base_locale
43
- @keys_missing_from_locale ||= {}
44
- @keys_missing_from_locale[locale] ||= begin
45
- keys = missing_tree(locale).key_names.map { |key| depluralize_key(key, locale) }.uniq
46
- KeyGroup.new keys, type: :missing_from_locale, locale: locale
47
- end
40
+ def eq_base_tree(locale, compare_to = base_locale)
41
+ base = data[compare_to].first.children
42
+ set_locale_tree_type data[locale].select_keys(root: false) { |key, node|
43
+ other_node = base[key]
44
+ other_node && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
45
+ }, locale, :eq_base
48
46
  end
49
47
 
50
- # @return [KeyGroup] keys missing value (but present in base)
51
- def keys_eq_base(locale)
52
- @keys_eq_base ||= {}
53
- @keys_eq_base[locale] ||= begin
54
- keys = data[base_locale].keys(root: false).map { |key, node|
55
- key if node.value == t(key, locale) && !ignore_key?(key, :eq_base, locale)
56
- }.compact
57
- KeyGroup.new keys, type: :eq_base, locale: locale
58
- end
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
+ }
59
54
  end
60
55
  end
61
56
  end
@@ -1,6 +1,19 @@
1
1
  # coding: utf-8
2
2
  module I18n::Tasks::PluralKeys
3
- PLURAL_KEY_RE = /\.(?:zero|one|two|few|many|other)$/
3
+ PLURAL_KEY_SUFFIXES = Set.new %w(zero one two few many other)
4
+ PLURAL_KEY_RE = /\.(?:#{PLURAL_KEY_SUFFIXES.to_a * '|'})$/
5
+
6
+ def collapse_plural_nodes!(tree)
7
+ tree.leaves.map(&:parent).uniq.each do |node|
8
+ children = node.children
9
+ if children.present? && children.all? { |c| PLURAL_KEY_SUFFIXES.include?(c.key) }
10
+ node.value = children.to_hash
11
+ node.data.merge!(children.first.data)
12
+ node.children = nil
13
+ end
14
+ end
15
+ tree
16
+ end
4
17
 
5
18
  # @param [String] key i18n key
6
19
  # @param [String] locale to pull key data from
@@ -10,6 +10,8 @@ module I18n::Tasks::Reports
10
10
  attr_reader :task
11
11
  delegate :base_locale, :locales, to: :task
12
12
 
13
+ protected
14
+
13
15
  MISSING_TYPES = {
14
16
  missing_from_base: {glyph: '✗', summary: 'missing from base locale'},
15
17
  missing_from_locale: {glyph: '∅', summary: 'missing from locale but present in base locale'},
@@ -20,18 +22,36 @@ module I18n::Tasks::Reports
20
22
  MISSING_TYPES
21
23
  end
22
24
 
23
- def missing_title(recs)
24
- "Missing translations (#{recs.length})"
25
+ def missing_title(forest)
26
+ "Missing translations (#{forest.leaves.count || '∅'})"
27
+ end
28
+
29
+ def unused_title(key_values)
30
+ "Unused keys (#{key_values.count || '∅'})"
31
+ end
32
+
33
+ def used_title(used_tree)
34
+ leaves = used_tree.leaves.to_a
35
+ filter = used_tree.first.root.data[:key_filter]
36
+ used_n = leaves.map { |node| node.data[:source_locations].size }.reduce(:+).to_i
37
+ "#{leaves.length} key#{'s' if leaves.size != 1}#{" ~ filter: '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
25
38
  end
26
39
 
27
- def unused_title(recs)
28
- "Unused keys (#{recs.length})"
40
+ # Sort keys by their attributes in order
41
+ # @param [Hash] order e.g. {locale: :asc, type: :desc, key: :asc}
42
+ def sort_by_attr!(objects, order)
43
+ order_keys = order.keys
44
+ objects.sort! { |a, b|
45
+ by = order_keys.detect { |by| a[by] != b[by] }
46
+ order[by] == :desc ? b[by] <=> a[by] : a[by] <=> b[by]
47
+ }
48
+ objects
29
49
  end
30
50
 
31
- def used_title(keys)
32
- filter = keys.attr[:key_filter]
33
- used_n = keys.map { |k| k[:usages].size }.reduce(:+).to_i
34
- "#{keys.length} key#{'s' if keys.size != 1}#{" ~ '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
51
+ def forest_to_attr(forest)
52
+ forest.keys(root: false).map { |key, node|
53
+ {key: key, type: node.data[:type], locale: node.root.key}
54
+ }
35
55
  end
36
56
  end
37
57
  end
@@ -19,17 +19,18 @@ module I18n::Tasks::Reports
19
19
  private
20
20
 
21
21
  def add_missing_sheet(wb)
22
- keys = task.missing_keys
22
+ tree = task.missing_keys
23
23
  wb.styles do |s|
24
24
  type_cell = s.add_style :alignment => {:horizontal => :center}
25
25
  locale_cell = s.add_style :alignment => {:horizontal => :center}
26
26
  regular_style = s.add_style
27
- wb.add_worksheet(name: missing_title(keys)) { |sheet|
27
+ wb.add_worksheet(name: missing_title(tree)) { |sheet|
28
28
  sheet.page_setup.fit_to :width => 1
29
29
  sheet.add_row ['Type', 'Locale', 'Key', 'Base Value']
30
30
  style_header sheet
31
- keys.each do |key|
32
- sheet.add_row [missing_types[key.type][:summary], key.locale, key.key, task.t(key)],
31
+ tree.keys do |key, node|
32
+ locale, type = node.root.data[:locale], node.data[:type]
33
+ sheet.add_row [missing_types[type][:summary], locale, key, task.t(key)],
33
34
  styles: [type_cell, locale_cell, regular_style, regular_style]
34
35
  end
35
36
  }
@@ -37,12 +38,12 @@ module I18n::Tasks::Reports
37
38
  end
38
39
 
39
40
  def add_unused_sheet(wb)
40
- keys = task.unused_keys
41
+ keys = task.unused_keys.root_key_values.sort { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] }
41
42
  wb.add_worksheet name: unused_title(keys) do |sheet|
42
- sheet.add_row ['Key', 'Base Value']
43
+ sheet.add_row ['Locale', 'Key', 'Value']
43
44
  style_header sheet
44
- keys.each do |key|
45
- sheet.add_row [key.key, task.t(key)]
45
+ keys.each do |locale_k_v|
46
+ sheet.add_row locale_k_v
46
47
  end
47
48
  end
48
49
  end
@@ -7,45 +7,49 @@ module I18n
7
7
  class Terminal < Base
8
8
  include Term::ANSIColor
9
9
 
10
- def missing_keys(keys = task.missing_keys)
11
- keys.sort_by_attr!(locale: :asc, type: :asc, key: :asc)
12
- print_title missing_title(keys)
13
- if keys.present?
10
+ def missing_keys(forest = task.missing_keys)
11
+ print_title missing_title(forest)
14
12
 
13
+ if forest.present?
15
14
  print_info "#{bold 'Types:'} #{missing_types.values.map { |t| "#{t[:glyph]} #{t[:summary]}" } * ', '}"
16
-
15
+ keys_data = sort_by_attr! forest_to_attr(forest), {locale: :asc, type: :asc, key: :asc}
17
16
  print_table headings: [magenta(bold('Locale')), bold('Type'), magenta(bold 'i18n Key'), bold(cyan "Base value (#{base_locale})")] do |t|
18
- t.rows = keys.map { |key|
19
- glyph = missing_types[key.type][:glyph]
20
- glyph = {missing_from_base: red(glyph), missing_from_locale: yellow(glyph), eq_base: bold(blue(glyph))}[key.type]
21
- if key[:type] == :missing_from_base
22
- locale = magenta key.locale
17
+ t.rows = keys_data.map do |d|
18
+ key = d[:key]
19
+ type = d[:type]
20
+ locale = d[:locale]
21
+ glyph = missing_types[type][:glyph]
22
+ glyph = {missing_from_base: red(glyph), missing_from_locale: yellow(glyph), eq_base: bold(blue(glyph))}[type]
23
+ if type == :missing_from_base
24
+ locale = magenta locale
23
25
  base_value = ''
24
26
  else
25
- locale = magenta key.locale
26
- base_value = task.t(key.key).to_s.strip
27
+ locale = magenta locale
28
+ base_value = task.t(key, base_locale).to_s.strip
27
29
  end
28
30
  [{value: locale, alignment: :center},
29
31
  {value: glyph, alignment: :center},
30
- magenta(key[:key]),
32
+ magenta(key),
31
33
  cyan(base_value)]
32
- }
34
+ end
33
35
  end
34
36
  else
35
- print_success 'Good job! No translations missing!'
37
+ print_success 'No translations missing!'
36
38
  end
37
39
  end
38
40
 
39
- def used_keys(keys = task.used_keys(src_locations: true))
40
- print_title used_title(keys)
41
- keys.sort_by_attr!(key: :asc)
42
- if keys.present?
43
- keys.each do |k|
44
- puts "#{bold "#{k.key}"} #{green(k.usages.size.to_s) if k.usages.size > 1}"
45
- k.usages.each do |u|
41
+ def used_keys(used_tree = task.used_tree(source_locations: true))
42
+ print_title used_title(used_tree)
43
+ keys_nodes = used_tree.keys.to_a
44
+ if keys_nodes.present?
45
+ keys_nodes.sort! { |a, b| a[0] <=> b[0] }
46
+ keys_nodes.each do |key, node|
47
+ usages = node.data[:source_locations]
48
+ puts "#{bold "#{key}"} #{green(usages.size.to_s) if usages.size > 1}"
49
+ usages.each do |u|
46
50
  line = u[:line].dup.tap { |line|
47
51
  line.strip!
48
- line.sub!(/(.*?)(#{k[:key]})(.*)$/) { dark($1) + underline($2) + dark($3)}
52
+ line.sub!(/(.*?)(#{key})(.*)$/) { dark($1) + underline($2) + dark($3) }
49
53
  }
50
54
  puts " #{green "#{u[:path]}:#{u[:line_num]}"} #{line}"
51
55
  end
@@ -55,15 +59,15 @@ module I18n
55
59
  end
56
60
  end
57
61
 
58
- def unused_keys(keys = task.unused_keys)
62
+ def unused_keys(tree = task.unused_keys)
63
+ keys = tree.root_key_values.sort { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] }
59
64
  print_title unused_title(keys)
60
- keys.sort_by_attr!(key: :asc)
61
65
  if keys.present?
62
- print_table headings: [bold(magenta('i18n Key')), bold(cyan("Base value (#{base_locale})"))] do |t|
63
- t.rows = keys.map { |k| [magenta(k.key), cyan(k.value)] }
66
+ print_table headings: [bold(magenta('Locale')), bold(magenta('i18n Key')), bold(cyan("Base value (#{base_locale})"))] do |t|
67
+ t.rows = keys.map { |(locale, k, v)| [magenta(locale), magenta(k), cyan(v.to_s)] }
64
68
  end
65
69
  else
66
- print_success 'Good job! Every translation is used!'
70
+ print_success 'Every translation is used!'
67
71
  end
68
72
  end
69
73
 
@@ -74,7 +78,7 @@ module I18n
74
78
  end
75
79
 
76
80
  def print_success(message)
77
- log_stderr(bold green message)
81
+ log_stderr(bold green ['Good job!', 'Well done!'].sample + ' ' + message)
78
82
  end
79
83
 
80
84
  def print_error(message)
@@ -10,7 +10,7 @@ module I18n::Tasks::Scanners
10
10
  attr_reader :config, :key_filter, :record_src_loc
11
11
 
12
12
  def initialize(config = {})
13
- @config = config.dup.with_indifferent_access.tap do |conf|
13
+ @config = config.dup.with_indifferent_access.tap do |conf|
14
14
  conf[:paths] = %w(app/) if conf[:paths].blank?
15
15
  conf[:include] = Array(conf[:include]) if conf[:include].present?
16
16
  if conf.key?(:exclude)
@@ -28,22 +28,23 @@ module I18n::Tasks::Scanners
28
28
  @key_filter_pattern = compile_key_pattern(value) if @key_filter
29
29
  end
30
30
 
31
- # @return [Array] found key usages, absolutized and unique
31
+ # @return [Array] keys used in source, absolutized and unique
32
32
  def keys
33
- @keys ||= (traverse_files { |path| scan_file(path) }.reduce(:+) || []).uniq(&:key)
33
+ @keys ||= (traverse_files { |path| scan_file(path) }.reduce(:+) || []).uniq(&:first)
34
34
  end
35
35
 
36
- def keys_with_src_locations
37
- with_src_locations do
36
+ # @return [Array<{key,usages}]
37
+ def keys_with_source_locations
38
+ recording_source_locations do
38
39
  keys = traverse_files { |path|
39
- ::I18n::Tasks::KeyGroup.new(scan_file(path), src_path: path)
40
+ scan_file(path)
40
41
  }.reduce(:+) || []
41
- keys.group_by(&:key).map { |key, key_loc|
42
- {key: key, usages: key_loc.map { |k| k[:src].merge(path: k[:src_path]) }}
42
+ keys.group_by(&:first).map { |key, key_loc|
43
+ [key, data: {source_locations: key_loc.map { |(k, attr)| attr[:data] }}]
43
44
  }
44
45
  end
45
46
  end
46
-
47
+
47
48
  def read_file(path)
48
49
  result = nil
49
50
  File.open(path, 'rb') { |f| result = f.read }
@@ -86,7 +87,7 @@ module I18n::Tasks::Scanners
86
87
  self.key_filter = filter_was
87
88
  end
88
89
 
89
- def with_src_locations
90
+ def recording_source_locations
90
91
  was = @record_src_loc
91
92
  @record_src_loc = true
92
93
  yield
@@ -100,16 +101,23 @@ module I18n::Tasks::Scanners
100
101
  globs.any? { |glob| File.fnmatch(glob, path) }
101
102
  end
102
103
 
103
- def src_location(text, src_pos)
104
- return nil unless @record_src_loc
104
+ def src_location(path, text, src_pos)
105
+ src = {path: path}
106
+ if @record_src_loc
107
+ src.merge!(src_text_location(text, src_pos))
108
+ end
109
+ src
110
+ end
111
+
112
+ def src_text_location(text, src_pos)
105
113
  line_begin = text.rindex(/^/, src_pos - 1)
106
114
  line_end = text.index(/.(?=\n|$)/, src_pos)
107
- {src: {
115
+ {
108
116
  pos: src_pos,
109
117
  line_num: text[0..src_pos].count("\n") + 1,
110
118
  line_pos: src_pos - line_begin + 1,
111
119
  line: text[line_begin..line_end]
112
- }}
120
+ }
113
121
  end
114
122
 
115
123
  # remove the leading colon and unwrap quotes from the key match
@@ -13,7 +13,7 @@ module I18n::Tasks::Scanners
13
13
  src_pos = Regexp.last_match.offset(0).first
14
14
  key = match_to_key(match, path)
15
15
  next unless valid_key?(key)
16
- keys << ::I18n::Tasks::Key.new(key, src_location(text, src_pos))
16
+ keys << [key, data: src_location(path, text, src_pos)]
17
17
  end
18
18
  keys
19
19
  end
@@ -34,6 +34,7 @@ module I18n::Tasks::Scanners
34
34
  # @return [String] full absolute key name
35
35
  def match_to_key(match, path)
36
36
  key = strip_literal(match[0])
37
+ key = key + '*' if key.end_with?('.')
37
38
  key = absolutize_key(key, path) if path && key.start_with?('.')
38
39
  key
39
40
  end
@@ -4,24 +4,24 @@ require 'set'
4
4
  module I18n
5
5
  module Tasks
6
6
  module UnusedKeys
7
- # @return [Array<[String, String]>] all the unused translations as an array of [key, value] pairs
8
- def unused_keys(locale = base_locale)
9
- @unused_keys ||= {}
10
- @unused_keys[locale] ||= begin
11
- keys = data[locale].keys(root: false).map { |key, value|
12
- next if used_in_expr?(key) || ignore_key?(key, :unused)
13
- key = depluralize_key(key, locale)
14
- key unless used_key?(key)
15
- }.compact.uniq
16
- KeyGroup.new keys, locale: locale, type: :unused
17
- end
7
+ def unused_keys(opts = {})
8
+ locales = Array(opts[:locales]).presence || self.locales
9
+ locales.map { |locale| unused_tree locale }.compact.reduce(:merge!)
10
+ end
11
+
12
+ def unused_tree(locale = base_locale)
13
+ collapse_plural_nodes! data[locale].select_keys { |key, _node|
14
+ !ignore_key?(key, :unused) &&
15
+ !used_in_expr?(key) &&
16
+ !used_key?(depluralize_key(key, locale))
17
+ }
18
18
  end
19
19
 
20
20
  def remove_unused!(locales = nil)
21
21
  locales ||= self.locales
22
- unused = unused_keys
23
22
  locales.each do |locale|
24
- data[locale] = data[locale].select_keys(root: false) { |key, value|
23
+ unused = unused_tree(locale).key_names.to_set
24
+ data[locale] = data[locale].select_keys { |key, value|
25
25
  !unused.include?(depluralize_key(key, locale))
26
26
  }
27
27
  end