i18n-tasks 0.4.5 → 0.5.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 (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