i18n-tasks 0.2.19 → 0.2.20

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +5 -1
  3. data/LICENSE.txt +1 -1
  4. data/README.md +51 -39
  5. data/doc/img/i18n-usages.png +0 -0
  6. data/i18n-tasks.gemspec +1 -1
  7. data/lib/i18n/tasks.rb +7 -2
  8. data/lib/i18n/tasks/base_task.rb +2 -4
  9. data/lib/i18n/tasks/configuration.rb +2 -4
  10. data/lib/i18n/tasks/data/adapter/json_adapter.rb +22 -0
  11. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +21 -0
  12. data/lib/i18n/tasks/data/file_system.rb +13 -0
  13. data/lib/i18n/tasks/data/storage/file_storage.rb +105 -0
  14. data/lib/i18n/tasks/data/yaml.rb +6 -65
  15. data/lib/i18n/tasks/data_traversal.rb +2 -2
  16. data/lib/i18n/tasks/fill_tasks.rb +2 -11
  17. data/lib/i18n/tasks/ignore_keys.rb +3 -1
  18. data/lib/i18n/tasks/key.rb +60 -0
  19. data/lib/i18n/tasks/key_group.rb +65 -0
  20. data/lib/i18n/tasks/missing_keys.rb +51 -18
  21. data/lib/i18n/tasks/reports/base.rb +8 -4
  22. data/lib/i18n/tasks/reports/spreadsheet.rb +8 -8
  23. data/lib/i18n/tasks/reports/terminal.rb +39 -16
  24. data/lib/i18n/tasks/scanners/base_scanner.rb +36 -2
  25. data/lib/i18n/tasks/scanners/pattern_scanner.rb +9 -7
  26. data/lib/i18n/tasks/translation_data.rb +4 -1
  27. data/lib/i18n/tasks/unused_keys.rb +9 -7
  28. data/lib/i18n/tasks/{source_keys.rb → used_keys.rb} +5 -6
  29. data/lib/i18n/tasks/version.rb +1 -1
  30. data/lib/tasks/i18n-tasks.rake +24 -20
  31. data/spec/file_system_data_spec.rb +68 -0
  32. data/spec/fixtures/config/i18n-tasks.yml +2 -2
  33. data/spec/i18n_tasks_spec.rb +5 -5
  34. data/spec/key_group_spec.rb +48 -0
  35. data/spec/used_keys_spec.rb +22 -0
  36. metadata +17 -7
  37. data/lib/i18n/tasks/untranslated_keys.rb +0 -50
  38. data/spec/yaml_adapter_spec.rb +0 -33
@@ -3,11 +3,11 @@ module I18n::Tasks::DataTraversal
3
3
  # @return [String,nil]
4
4
  def t(hash = data[base_locale], key)
5
5
  if hash.is_a?(String)
6
- # has is a locale
6
+ # hash is a locale
7
7
  raise ArgumentError.new("invalid locale: #{hash}") if hash =~ /[^A-z\d-]/
8
8
  hash = data[hash]
9
9
  end
10
- key.split('.').inject(hash) { |r, seg| r[seg] if r }
10
+ key.to_s.split('.').inject(hash) { |r, seg| r[seg] if r }
11
11
  end
12
12
 
13
13
  # traverse => map if yield(k, v)
@@ -22,7 +22,7 @@ module I18n::Tasks::FillTasks
22
22
  locales = non_base_locales(locales)
23
23
  normalize_store! locales
24
24
  locales.each do |locale|
25
- blank_keys = find_blank_keys(locale).select { |k|
25
+ blank_keys = keys_missing_from_locale(locale).key_names.select { |k|
26
26
  tr = t(k)
27
27
  tr.present? && tr.is_a?(String)
28
28
  }
@@ -48,17 +48,8 @@ module I18n::Tasks::FillTasks
48
48
  # fill blank values with values from passed block
49
49
  # @param [String] locale
50
50
  def set_blank_values!(locale = base_locale, &fill_with)
51
- blank_keys = find_blank_keys locale
51
+ blank_keys = keys_missing_from_locale(locale).key_names
52
52
  list = blank_keys.zip fill_with.call(blank_keys)
53
53
  data[locale] = data[locale].deep_merge(list_to_tree(list))
54
54
  end
55
-
56
-
57
- def find_blank_keys(locale, include_missing = (locale == base_locale))
58
- blank_keys = traverse_map_if(data[base_locale]) { |key, value|
59
- key if !key_value?(key, locale) && !ignore_key?(key, :missing)
60
- }
61
- blank_keys += keys_not_in_base if include_missing
62
- blank_keys.uniq
63
- end
64
55
  end
@@ -10,7 +10,9 @@ module I18n::Tasks::IgnoreKeys
10
10
  # @param locale [String] only when type is :eq_base
11
11
  # @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
12
12
  def ignore_pattern(type, locale = nil)
13
- ((@ignore_patterns ||= HashWithIndifferentAccess.new)[type] ||= {})[locale] = begin
13
+ @ignore_patterns ||= HashWithIndifferentAccess.new
14
+ @ignore_patterns[type] ||= {}
15
+ @ignore_patterns[type][locale] ||= begin
14
16
  global, type_ignore = config[:ignore].presence || [], config["ignore_#{type}"].presence || []
15
17
  if type_ignore.is_a?(Array)
16
18
  patterns = global + type_ignore
@@ -0,0 +1,60 @@
1
+ module I18n
2
+ module Tasks
3
+ class Key
4
+ attr_accessor :own_attr, :key_group
5
+
6
+ def initialize(key_or_attr, own_attr = {})
7
+ @own_attr = if key_or_attr.is_a?(Array)
8
+ {key: key_or_attr[0], value: key_or_attr[1]}.merge(own_attr)
9
+ elsif key_or_attr.is_a?(Hash)
10
+ key_or_attr.merge(own_attr)
11
+ else
12
+ own_attr.merge(key: key_or_attr)
13
+ end
14
+
15
+ @own_attr[:key] = @own_attr[:key].to_s
16
+ end
17
+
18
+ def [](prop)
19
+ @own_attr[prop] || key_group.attr[prop]
20
+ end
21
+
22
+ def attr
23
+ key_group.attr.merge @own_attr
24
+ end
25
+
26
+ def ==(other)
27
+ self.attr == other.attr
28
+ end
29
+
30
+ def inspect
31
+ "#<#{self.class.name}#{attr.inspect}>"
32
+ end
33
+
34
+ def clone_orphan
35
+ clone.tap { |k| k.key_group = nil }
36
+ end
37
+
38
+ def key
39
+ @own_attr[:key]
40
+ end
41
+ alias to_s key
42
+
43
+ def value
44
+ self[:value]
45
+ end
46
+
47
+ def locale
48
+ self[:locale]
49
+ end
50
+
51
+ def type
52
+ self[:type]
53
+ end
54
+
55
+ def src_pos
56
+ self[:src_pos]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,65 @@
1
+ require 'set'
2
+ module I18n
3
+ module Tasks
4
+ class KeyGroup
5
+ attr_reader :keys, :attr, :key_names
6
+
7
+ delegate :size, :length, :each, :[], to: :keys
8
+ include Enumerable
9
+
10
+ def initialize(keys, attr = {})
11
+ @keys = if keys && !keys[0].is_a?(::I18n::Tasks::Key)
12
+ keys.map { |key| I18n::Tasks::Key.new(key) }
13
+ else
14
+ keys
15
+ end
16
+ @keys.each { |key| key.key_group ||= self } unless attr.delete(:orphan)
17
+
18
+ @keys_by_name = @keys.inject({}) { |h, k| h[k.key.to_s] = k; h }
19
+ @key_names = @keys.map(&:key)
20
+ @attr = attr
21
+ end
22
+
23
+ def find_by_name(key)
24
+ @keys_by_name[key.to_s]
25
+ end
26
+
27
+ def key_names_set
28
+ @key_names_set ||= Set.new(@key_names)
29
+ end
30
+
31
+ def include?(key)
32
+ key_names_set.include?(key.to_s)
33
+ end
34
+
35
+ # order, e.g: {locale: :asc, type: :desc, key: :asc}
36
+ def sort!(&block)
37
+ @keys.sort!(&block)
38
+ @key_names = @keys.map(&:to_s)
39
+ self
40
+ end
41
+
42
+ def sort_by_attr!(order)
43
+ order_keys = order.keys
44
+ 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
+ self
49
+ end
50
+
51
+ def to_a
52
+ @array ||= keys.map(&:attr)
53
+ end
54
+
55
+ alias as_json to_a
56
+
57
+ def merge(other)
58
+ KeyGroup.new(keys + other.keys,
59
+ type: [attr[:type], other.attr[:type]].flatten.compact)
60
+ end
61
+
62
+ alias + merge
63
+ end
64
+ end
65
+ end
@@ -1,23 +1,56 @@
1
- module I18n::Tasks::MissingKeys
2
- # @return Array missing keys, i.e. key that are in the code but are not in the base locale data
3
- def keys_not_in_base
4
- find_source_keys.reject { |key|
5
- key_value?(key, base_locale) || pattern_key?(key) || ignore_key?(key, :missing)
6
- }
7
- end
1
+ module I18n::Tasks
2
+ module MissingKeys
3
+ # @param [:missing_from_base, :missing_from_locale, :eq_base] type (default nil)
4
+ # @return [KeyGroup]
5
+ def missing_keys(opts = {})
6
+ type = opts[:type]
7
+ locales = non_base_locales(opts[:locales])
8
+ if type
9
+ if type == :missing_from_base
10
+ keys_missing_from_base
11
+ else
12
+ locales.map { |locale| send("keys_#{type}", locale) }.reduce(:+)
13
+ end
14
+ else
15
+ missing_keys(type: :missing_from_base) +
16
+ missing_keys(type: :eq_base, locales: locales) +
17
+ missing_keys(type: :missing_from_locale, locales: locales)
18
+ end
19
+ end
8
20
 
9
- # @return Array keys missing (nil or blank?) in locale but present in base
10
- def keys_blank_in_locale(locale)
11
- traverse_map_if data[base_locale] do |key, base_value|
12
- key if !ignore_key?(key, :missing) && !key_value?(key, locale) && !key_value?(depluralize_key(key), locale)
21
+ def untranslated_keys(locales = nil)
22
+ I18n::Tasks.warn_deprecated("#untranslated_keys. Please use #missing_keys instead")
23
+ missing_keys(locales: locales)
13
24
  end
14
- end
15
25
 
16
- # @return Array keys missing value (but present in base)
17
- def keys_eq_base(locale)
18
- traverse_map_if data[base_locale] do |key, base_value|
19
- key if base_value == t(locale, key) && !ignore_key?(key, :eq_base, locale)
26
+ # @return [KeyGroup] missing keys, i.e. key that are in the code but are not in the base locale data
27
+ def keys_missing_from_base
28
+ @keys_missing_from_base ||= begin
29
+ KeyGroup.new(
30
+ used_keys.keys.reject { |k|
31
+ key = k.key
32
+ key_value?(key, base_locale) || pattern_key?(key) || ignore_key?(key, :missing)
33
+ }.map(&:clone_orphan), type: :missing_from_base, locale: base_locale)
34
+ end
20
35
  end
21
- end
22
36
 
23
- end
37
+ # @return [KeyGroup] keys missing (nil or blank?) in locale but present in base
38
+ def keys_missing_from_locale(locale)
39
+ return keys_missing_from_base if locale == base_locale
40
+ @keys_missing_from_locale ||= {}
41
+ @keys_missing_from_locale[locale] ||= KeyGroup.new(
42
+ traverse_map_if(data[base_locale]) { |key, base_value|
43
+ key if !ignore_key?(key, :missing) && !key_value?(key, locale) && !key_value?(depluralize_key(key), locale)
44
+ }, type: :missing_from_locale, locale: locale)
45
+
46
+ end
47
+
48
+ # @return [KeyGroup] keys missing value (but present in base)
49
+ def keys_eq_base(locale)
50
+ @keys_eq_base ||= KeyGroup.new(
51
+ traverse_map_if(data[base_locale]) { |key, base_value|
52
+ key if base_value == t(locale, key) && !ignore_key?(key, :eq_base, locale)
53
+ }, type: :eq_base, locale: locale)
54
+ end
55
+ end
56
+ end
@@ -9,9 +9,9 @@ module I18n::Tasks::Reports
9
9
  attr_reader :task
10
10
 
11
11
  MISSING_TYPES = {
12
- none: {glyph: '✗', summary: 'key missing'},
13
- blank: {glyph: '∅', summary: 'translation blank'},
14
- eq_base: {glyph: '=', summary: 'value same as base value'}
12
+ missing_from_base: {glyph: '✗', summary: 'missing from base locale'},
13
+ missing_from_locale: {glyph: '∅', summary: 'missing from locale but present in base locale'},
14
+ eq_base: {glyph: '=', summary: 'value equals base value'}
15
15
  }
16
16
 
17
17
  def missing_types
@@ -25,5 +25,9 @@ module I18n::Tasks::Reports
25
25
  def unused_title(recs)
26
26
  "Unused keys (#{recs.length})"
27
27
  end
28
+
29
+ def used_title(keys)
30
+ "#{keys.length} keys used #{keys.map { |k| k[:usages].size }.reduce(:+)} times"
31
+ end
28
32
  end
29
- end
33
+ end
@@ -17,17 +17,17 @@ module I18n::Tasks::Reports
17
17
  private
18
18
 
19
19
  def add_missing_sheet(wb)
20
- recs = task.untranslated_keys
20
+ keys = task.missing_keys
21
21
  wb.styles do |s|
22
22
  type_cell = s.add_style :alignment => {:horizontal => :center}
23
23
  locale_cell = s.add_style :alignment => {:horizontal => :center}
24
24
  regular_style = s.add_style
25
- wb.add_worksheet(name: missing_title(recs)) { |sheet|
25
+ wb.add_worksheet(name: missing_title(keys)) { |sheet|
26
26
  sheet.page_setup.fit_to :width => 1
27
27
  sheet.add_row ['Type', 'Locale', 'Key', 'Base Value']
28
28
  style_header sheet
29
- recs.each do |rec|
30
- sheet.add_row [missing_types[rec[:type]][:summary], rec[:locale], rec[:key], rec[:base_value]],
29
+ keys.each do |key|
30
+ sheet.add_row [missing_types[key.type][:summary], key.locale, key.key, task.t(key)],
31
31
  styles: [type_cell, locale_cell, regular_style, regular_style]
32
32
  end
33
33
  }
@@ -35,12 +35,12 @@ module I18n::Tasks::Reports
35
35
  end
36
36
 
37
37
  def add_unused_sheet(wb)
38
- recs = task.unused_keys
39
- wb.add_worksheet name: unused_title(recs) do |sheet|
38
+ keys = task.unused_keys
39
+ wb.add_worksheet name: unused_title(keys) do |sheet|
40
40
  sheet.add_row ['Key', 'Base Value']
41
41
  style_header sheet
42
- recs.each do |rec|
43
- sheet.add_row rec
42
+ keys.each do |key|
43
+ sheet.add_row [key.key, task.t(key)]
44
44
  end
45
45
  end
46
46
  end
@@ -7,26 +7,27 @@ module I18n
7
7
  class Terminal < Base
8
8
  include Term::ANSIColor
9
9
 
10
- def missing_translations(recs = task.untranslated_keys)
11
- print_title missing_title(recs)
12
- if recs.present?
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?
13
14
 
14
15
  $stderr.puts "#{bold 'Types:'} #{missing_types.values.map { |t| "#{t[:glyph]} #{t[:summary]}" } * ', '}"
15
16
 
16
17
  print_table headings: [magenta(bold('Locale')), bold('Type'), magenta('i18n Key'), bold(cyan "Base value (#{base_locale})")] do |t|
17
- t.rows = recs.map { |rec|
18
- glyph = missing_types[rec[:type]][:glyph]
19
- glyph = {none: red(glyph), blank: yellow(glyph), eq_base: bold(blue(glyph))}[rec[:type]]
20
- if rec[:type] == :none
21
- locale = magenta bold rec[:locale]
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 bold key.locale
22
23
  base_value = ''
23
24
  else
24
- locale = magenta rec[:locale]
25
- base_value = cyan rec[:base_value].to_s.strip
25
+ locale = magenta key.locale
26
+ base_value = task.t(key.key).to_s.strip
26
27
  end
27
28
  [{value: locale, alignment: :center},
28
29
  {value: glyph, alignment: :center},
29
- magenta(rec[:key]),
30
+ magenta(key[:key]),
30
31
  base_value]
31
32
  }
32
33
  end
@@ -35,11 +36,30 @@ module I18n
35
36
  end
36
37
  end
37
38
 
38
- def unused_translations(recs = task.unused_keys)
39
- print_title unused_title(recs)
40
- if recs.present?
39
+ def used_keys(keys = task.used_keys)
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} (#{k[:usages].size}):"
45
+ k[:usages].each do |u|
46
+ line = faint u[:line].dup.tap { |line|
47
+ line.sub!(k[:key], underline(k[:key]))
48
+ line.strip!
49
+ }
50
+ puts " #{u[:path]}:#{u[:line_num]}: #{faint line}"
51
+ end
52
+ end
53
+ else
54
+ print_error 'No key usages found. Please check configuration in config/i18n-tasks.yml'
55
+ end
56
+ end
57
+
58
+ def unused_keys(keys = task.unused_keys)
59
+ print_title unused_title(keys)
60
+ if keys.present?
41
61
  print_table headings: [bold(magenta('i18n Key')), cyan("Base value (#{base_locale})")] do |t|
42
- t.rows = recs.map { |x| [magenta(x[0]), cyan(x[1])] }
62
+ t.rows = keys.map { |k| [magenta(k.key), cyan(k.value)] }
43
63
  end
44
64
  else
45
65
  print_success 'Good job! Every translation is used!'
@@ -56,7 +76,10 @@ module I18n
56
76
  $stderr.puts(bold green message)
57
77
  end
58
78
 
59
- private
79
+ def print_error(message)
80
+ $stderr.puts(bold red message)
81
+ end
82
+
60
83
  def indent(txt, n = 2)
61
84
  spaces = ' ' * n
62
85
  txt.gsub /^/, spaces
@@ -14,11 +14,36 @@ module I18n::Tasks::Scanners
14
14
 
15
15
  # @return [Array] found key usages, absolutized and unique
16
16
  def keys
17
- @keys ||= traverse_files { |path| scan_file(path) }.flatten.uniq
17
+ @keys ||= begin
18
+ @keys_by_name = {}
19
+ keys = []
20
+ usages = traverse_files { |path|
21
+ ::I18n::Tasks::KeyGroup.new(scan_file(path), src_path: path) }.map(&:keys).flatten
22
+ usages.group_by(&:key).each do |key, instances|
23
+ key = {
24
+ key: key,
25
+ usages: instances.map { |inst| inst[:src].merge(path: inst[:src_path]) }
26
+ }
27
+ @keys_by_name[key.to_s] = key
28
+ keys << key
29
+ end
30
+ keys
31
+ end
32
+ end
33
+
34
+ def key_usages(key)
35
+ keys
36
+ @keys_by_name[key.to_s][:usages]
37
+ end
38
+
39
+ def read_file(path)
40
+ result = nil
41
+ File.open(path, 'rb') { |f| result = f.read }
42
+ result
18
43
  end
19
44
 
20
45
  # @return [String] keys used in file (unimplemented)
21
- def scan_file(path)
46
+ def scan_file(path, *args)
22
47
  raise 'Unimplemented'
23
48
  end
24
49
 
@@ -37,6 +62,15 @@ module I18n::Tasks::Scanners
37
62
 
38
63
  protected
39
64
 
65
+ def usage_context(text, src_pos)
66
+ line_begin = text.rindex(/^/, src_pos - 1)
67
+ line_end = text.index(/.(?=\n|$)/, src_pos)
68
+ {pos: src_pos,
69
+ line_num: text[0..src_pos].count("\n") + 1,
70
+ line_pos: src_pos - line_begin + 1,
71
+ line: text[line_begin..line_end]}
72
+ end
73
+
40
74
  def extract_key_from_match(match, path)
41
75
  key = strip_literal(match[0])
42
76
  key = absolutize_key(key, path) if path && key.start_with?('.')