i18n-tasks 0.2.19 → 0.2.20

Sign up to get free protection for your applications and to get access to all the features.
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?('.')