i18n-tasks 0.2.22 → 0.3.0.rc1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +5 -1
  3. data/Gemfile +0 -1
  4. data/README.md +59 -49
  5. data/bin/i18n-tasks +38 -0
  6. data/i18n-tasks.gemspec +1 -0
  7. data/lib/i18n/tasks/commands.rb +121 -0
  8. data/lib/i18n/tasks/commands_base.rb +54 -0
  9. data/lib/i18n/tasks/configuration.rb +39 -1
  10. data/lib/i18n/tasks/data/storage/file_storage.rb +1 -1
  11. data/lib/i18n/tasks/data_traversal.rb +6 -7
  12. data/lib/i18n/tasks/fill_tasks.rb +20 -48
  13. data/lib/i18n/tasks/google_translation.rb +1 -1
  14. data/lib/i18n/tasks/key.rb +11 -26
  15. data/lib/i18n/tasks/key/key_group.rb +44 -0
  16. data/lib/i18n/tasks/key/match_pattern.rb +23 -0
  17. data/lib/i18n/tasks/key/usages.rb +11 -0
  18. data/lib/i18n/tasks/key_pattern_matching.rb +6 -2
  19. data/lib/i18n/tasks/missing_keys.rb +15 -12
  20. data/lib/i18n/tasks/plural_keys.rb +3 -3
  21. data/lib/i18n/tasks/reports/base.rb +3 -2
  22. data/lib/i18n/tasks/reports/spreadsheet.rb +2 -1
  23. data/lib/i18n/tasks/reports/terminal.rb +6 -6
  24. data/lib/i18n/tasks/scanners/base_scanner.rb +20 -14
  25. data/lib/i18n/tasks/scanners/pattern_scanner.rb +31 -5
  26. data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +75 -0
  27. data/lib/i18n/tasks/translation_data.rb +32 -11
  28. data/lib/i18n/tasks/unused_keys.rb +3 -2
  29. data/lib/i18n/tasks/used_keys.rb +14 -11
  30. data/lib/i18n/tasks/version.rb +1 -1
  31. data/lib/tasks/i18n-tasks.rake +34 -85
  32. data/spec/fixtures/app/controllers/events_controller.rb +23 -3
  33. data/spec/fixtures/app/views/index.html.slim +4 -1
  34. data/spec/fixtures/app/views/usages.html.slim +2 -0
  35. data/spec/fixtures/config/i18n-tasks.yml +1 -1
  36. data/spec/i18n_tasks_spec.rb +66 -38
  37. data/spec/pattern_scanner_spec.rb +1 -1
  38. data/spec/spec_helper.rb +2 -1
  39. data/spec/support/capture_std.rb +17 -0
  40. data/spec/support/fixtures.rb +9 -2
  41. data/spec/support/test_codebase.rb +5 -18
  42. data/spec/support/test_codebase_env.rake +4 -2
  43. data/spec/used_keys_spec.rb +1 -0
  44. metadata +31 -5
@@ -44,7 +44,7 @@ module I18n::Tasks
44
44
  hash.deep_merge! locale_data || {}
45
45
  hash
46
46
  end[locale.to_s] || {}
47
- end
47
+ end.with_indifferent_access
48
48
  end
49
49
 
50
50
  alias [] get
@@ -1,13 +1,12 @@
1
1
  module I18n::Tasks::DataTraversal
2
2
  # translation of the key found in the passed hash or nil
3
3
  # @return [String,nil]
4
- def t(hash = data[base_locale], key)
5
- if hash.is_a?(String)
6
- # hash is a locale
7
- raise ArgumentError.new("invalid locale: #{hash}") if hash =~ /[^A-z\d-]/
8
- hash = data[hash]
9
- end
10
- key.to_s.split('.').inject(hash) { |r, seg| r[seg] if r }
4
+ def t(key, locale = base_locale)
5
+ key.to_s.split('.').inject(self.data[locale]) { |r, seg| r[seg] if r }
6
+ end
7
+
8
+ def t_proc(locale = base_locale)
9
+ proc { |k| t(k, locale) }
11
10
  end
12
11
 
13
12
  # traverse => map if yield(k, v)
@@ -1,55 +1,27 @@
1
1
  module I18n::Tasks::FillTasks
2
- def add_missing!(locale = base_locale, placeholder = nil)
3
- normalize_store! locale
4
- set_blank_values! locale do |keys|
5
- keys.map { |key|
6
- placeholder || key.split('.').last.to_s.humanize
7
- }
8
- end
2
+ def fill_missing_value(opts = {})
3
+ opts = opts.merge(
4
+ keys: proc { |locale| keys_to_fill(locale) }
5
+ )
6
+ opts[:value] ||= '' unless opts[:values].present?
7
+ update_data opts
9
8
  end
10
9
 
11
- def fill_with_blanks!(locales = nil)
12
- locales = non_base_locales(locales)
13
- add_missing! base_locale, ''
14
- normalize_store! locales
15
- locales.each do |locale|
16
- add_missing! locale, ''
17
- end
10
+ def fill_missing_google_translate(opts = {})
11
+ from = opts[:from] || base_locale
12
+ opts = opts.merge(
13
+ locales: non_base_locales(opts[:locales]),
14
+ keys: proc { |locale| keys_to_fill(locale).select(&t_proc(from)).select { |k| t(k).is_a?(String) } },
15
+ values: proc { |keys, locale|
16
+ google_translate keys.zip(keys.map(&t_proc(from))),
17
+ to: locale,
18
+ from: from
19
+ }
20
+ )
21
+ update_data opts
18
22
  end
19
23
 
20
- def fill_with_google_translate!(locales = nil)
21
- normalize_store! base_locale
22
- locales = non_base_locales(locales)
23
- normalize_store! locales
24
- locales.each do |locale|
25
- blank_keys = keys_missing_from_locale(locale).key_names.select { |k|
26
- tr = t(k)
27
- tr.present? && tr.is_a?(String)
28
- }
29
- if blank_keys.present?
30
- data[locale] = data[locale].deep_merge(
31
- list_to_tree google_translate(blank_keys.zip(blank_keys.map { |k| t(k) }), to: locale, from: base_locale)
32
- )
33
- end
34
- end
35
- end
36
-
37
- def fill_with_base_values!(locales = nil)
38
- normalize_store! base_locale
39
- locales = non_base_locales(locales)
40
- normalize_store! locales
41
- locales.each do |locale|
42
- set_blank_values! locale do |blank_keys|
43
- blank_keys.map { |k| t(k) }
44
- end
45
- end
46
- end
47
-
48
- # fill blank values with values from passed block
49
- # @param [String] locale
50
- def set_blank_values!(locale = base_locale, &fill_with)
51
- blank_keys = keys_missing_from_locale(locale).key_names
52
- list = blank_keys.zip fill_with.call(blank_keys)
53
- data[locale] = data[locale].deep_merge(list_to_tree(list))
24
+ def keys_to_fill(locale)
25
+ keys_missing_from_locale(locale).key_names
54
26
  end
55
27
  end
@@ -9,7 +9,7 @@ module I18n::Tasks::GoogleTranslation
9
9
  opts[:key] = key
10
10
  end
11
11
  if opts[:key].blank?
12
- $stderr.puts(Term::ANSIColor.red Term::ANSIColor.yellow 'You may need to provide Google API key as GOOGLE_TRANSLATE_API_KEY or in config/i18n-tasks.yml.
12
+ $stderr.puts(Term::ANSIColor.red Term::ANSIColor.yellow 'You may need to provide Google API key as GOOGLE_TRANSLATE_API_KEY env var or translation.api_key in config/i18n-tasks.yml.
13
13
  You can obtain the key at https://code.google.com/apis/console.')
14
14
  end
15
15
  list.group_by { |k_v| k_v[0].end_with?('_html'.freeze) ? opts.merge(html: true) : opts.merge(format: 'text') }.map do |opts, strings|
@@ -1,7 +1,15 @@
1
+ require 'i18n/tasks/key/key_group'
2
+ require 'i18n/tasks/key/match_pattern'
3
+ require 'i18n/tasks/key/usages'
4
+
1
5
  module I18n
2
6
  module Tasks
3
7
  class Key
4
- attr_accessor :own_attr, :key_group
8
+ include ::I18n::Tasks::Key::KeyGroup
9
+ include ::I18n::Tasks::Key::MatchPattern
10
+ include ::I18n::Tasks::Key::Usages
11
+
12
+ attr_accessor :own_attr
5
13
 
6
14
  def initialize(key_or_attr, own_attr = {})
7
15
  @own_attr = if key_or_attr.is_a?(Array)
@@ -14,14 +22,6 @@ module I18n
14
22
  @own_attr[:key] = @own_attr[:key].to_s
15
23
  end
16
24
 
17
- def [](prop)
18
- @own_attr[prop] || key_group.attr[prop]
19
- end
20
-
21
- def attr
22
- key_group.attr.merge @own_attr
23
- end
24
-
25
25
  def ==(other)
26
26
  self.attr == other.attr
27
27
  end
@@ -30,29 +30,14 @@ module I18n
30
30
  "#<#{self.class.name}#{attr.inspect}>"
31
31
  end
32
32
 
33
- def clone_orphan
34
- clone.tap { |k| k.key_group = nil }
35
- end
36
-
37
33
  def key
38
34
  @own_attr[:key]
39
35
  end
36
+
40
37
  alias to_s key
41
38
 
42
39
  def value
43
- self[:value]
44
- end
45
-
46
- def locale
47
- self[:locale]
48
- end
49
-
50
- def type
51
- self[:type]
52
- end
53
-
54
- def src_pos
55
- self[:src_pos]
40
+ @own_attr[:value]
56
41
  end
57
42
  end
58
43
  end
@@ -0,0 +1,44 @@
1
+ module I18n
2
+ module Tasks
3
+ class Key
4
+ module KeyGroup
5
+ attr_accessor :key_group
6
+
7
+ def self.included(base)
8
+ base.class_eval do
9
+ extend ClassMethods
10
+ delegate_to_attr :[], :key?
11
+ delegate_to_attr_accessor :type, :locale
12
+ end
13
+ end
14
+
15
+ def attr
16
+ key_group.attr.merge @own_attr
17
+ end
18
+
19
+ def clone_orphan
20
+ clone.tap { |k| k.key_group = nil }
21
+ end
22
+
23
+ module ClassMethods
24
+ def delegate_to_attr_accessor(*methods)
25
+ methods.each do |m|
26
+ define_method(m) do
27
+ @own_attr[m] || (kg = key_group) && kg.attr[m]
28
+ end
29
+ end
30
+ end
31
+
32
+ def delegate_to_attr(*methods)
33
+ methods.each do |m|
34
+ define_method(m) do |*args|
35
+ @own_attr.send(m, *args) ||
36
+ (kg = key_group) && kg.attr.send(m, *args)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ module I18n
2
+ module Tasks
3
+ class Key
4
+ module MatchPattern
5
+ def key_match_pattern
6
+ @key_match_pattern ||= begin
7
+ k = key
8
+ "#{k.gsub(/\#{.*?}/, '*')}#{'*' if k.end_with?('.')}"
9
+ end
10
+ end
11
+
12
+ # A key interpolated with expression
13
+ def expr?
14
+ if @is_expr.nil?
15
+ k = key
16
+ @is_expr = (k =~ /\#{.*?}/ || k.end_with?('.'))
17
+ end
18
+ @is_expr
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module I18n
2
+ module Tasks
3
+ class Key
4
+ module Usages
5
+ def usages
6
+ self[:usages]
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -17,12 +17,16 @@ module I18n::Tasks::KeyPatternMatching
17
17
  # : matches a single key
18
18
  # {a, b.c} match any in set, can use : and *, match is captured
19
19
  def compile_key_pattern(key_pattern)
20
- /^#{key_pattern.
20
+ return key_pattern if key_pattern.is_a?(Regexp)
21
+ /\A#{key_pattern_re_body(key_pattern)}\z/
22
+ end
23
+
24
+ def key_pattern_re_body(key_pattern)
25
+ key_pattern.
21
26
  gsub(/\./, '\.').
22
27
  gsub(/\*/, '.*').
23
28
  gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)').
24
29
  gsub(/\{(.*?)}/) { "(#{$1.strip.gsub /\s*,\s*/, '|'})" }
25
- }$/
26
30
  end
27
31
 
28
32
  # @return [Array<String>] keys sans passed patterns
@@ -3,21 +3,24 @@ module I18n::Tasks
3
3
  # @param [:missing_from_base, :missing_from_locale, :eq_base] type (default nil)
4
4
  # @return [KeyGroup]
5
5
  def missing_keys(opts = {})
6
- type = opts[:type]
7
6
  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(:+) || KeyGroup.new([])
13
- end
7
+ type = opts[:type]
8
+ unless type
9
+ types = opts[:types] || missing_keys_types
10
+ opts = opts.except(:types).merge(locales: locales)
11
+ return types.map { |t| missing_keys(opts.merge(type: t)) }.reduce(:+)
12
+ end
13
+ if type == :missing_from_base
14
+ keys_missing_from_base
14
15
  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)
16
+ locales.map { |locale| send("keys_#{type}", locale) }.reduce(:+) || KeyGroup.new([])
18
17
  end
19
18
  end
20
19
 
20
+ def missing_keys_types
21
+ @missing_keys_types ||= [:missing_from_base, :eq_base, :missing_from_locale]
22
+ end
23
+
21
24
  def untranslated_keys(locales = nil)
22
25
  I18n::Tasks.warn_deprecated("#untranslated_keys. Please use #missing_keys instead")
23
26
  missing_keys(locales: locales)
@@ -29,7 +32,7 @@ module I18n::Tasks
29
32
  KeyGroup.new(
30
33
  used_keys.keys.reject { |k|
31
34
  key = k.key
32
- key_value?(key, base_locale) || pattern_key?(key) || ignore_key?(key, :missing)
35
+ k.expr? || key_value?(key, base_locale) || ignore_key?(key, :missing)
33
36
  }.map(&:clone_orphan), type: :missing_from_base, locale: base_locale)
34
37
  end
35
38
  end
@@ -49,7 +52,7 @@ module I18n::Tasks
49
52
  def keys_eq_base(locale)
50
53
  @keys_eq_base ||= KeyGroup.new(
51
54
  traverse_map_if(data[base_locale]) { |key, base_value|
52
- key if base_value == t(locale, key) && !ignore_key?(key, :eq_base, locale)
55
+ key if base_value == t(key, locale) && !ignore_key?(key, :eq_base, locale)
53
56
  }, type: :eq_base, locale: locale)
54
57
  end
55
58
  end
@@ -5,13 +5,13 @@ module I18n::Tasks::PluralKeys
5
5
  # @param [String] locale to pull key data from
6
6
  # @return the base form if the key is a specific plural form (e.g. apple for apple.many), and the key as passed otherwise
7
7
  def depluralize_key(locale = base_locale, key)
8
- return key if key !~ PLURAL_KEY_RE || t(locale, key).is_a?(Hash)
8
+ return key if key !~ PLURAL_KEY_RE || t(key, locale).is_a?(Hash)
9
9
  parent_key = key.split('.')[0..-2] * '.'
10
- plural_versions = t(locale, parent_key)
10
+ plural_versions = t(parent_key, locale)
11
11
  if plural_versions.is_a?(Hash) && plural_versions.all? { |k, v| ".#{k}" =~ PLURAL_KEY_RE && !v.is_a?(Hash) }
12
12
  parent_key
13
13
  else
14
14
  key
15
15
  end
16
16
  end
17
- end
17
+ end
@@ -2,11 +2,12 @@
2
2
  module I18n::Tasks::Reports
3
3
  class Base
4
4
 
5
- def initialize
6
- @task = I18n::Tasks::BaseTask.new
5
+ def initialize(task = I18n::Tasks::BaseTask.new)
6
+ @task = task
7
7
  end
8
8
 
9
9
  attr_reader :task
10
+ delegate :base_locale, :locales, to: :task
10
11
 
11
12
  MISSING_TYPES = {
12
13
  missing_from_base: {glyph: '✗', summary: 'missing from base locale'},
@@ -4,7 +4,8 @@ require 'fileutils'
4
4
  module I18n::Tasks::Reports
5
5
  class Spreadsheet < Base
6
6
 
7
- def save_report(path = 'tmp/i18n-report.xlsx')
7
+ def save_report(path = nil)
8
+ path = 'tmp/i18n-report.xlsx' if path.blank?
8
9
  p = Axlsx::Package.new
9
10
  add_missing_sheet p.workbook
10
11
  add_unused_sheet p.workbook
@@ -14,12 +14,12 @@ module I18n
14
14
 
15
15
  print_info "#{bold 'Types:'} #{missing_types.values.map { |t| "#{t[:glyph]} #{t[:summary]}" } * ', '}"
16
16
 
17
- print_table headings: [magenta(bold('Locale')), bold('Type'), magenta('i18n Key'), bold(cyan "Base value (#{base_locale})")] do |t|
17
+ print_table headings: [magenta(bold('Locale')), bold('Type'), magenta(bold 'i18n Key'), bold(cyan "Base value (#{base_locale})")] do |t|
18
18
  t.rows = keys.map { |key|
19
19
  glyph = missing_types[key.type][:glyph]
20
20
  glyph = {missing_from_base: red(glyph), missing_from_locale: yellow(glyph), eq_base: bold(blue(glyph))}[key.type]
21
21
  if key[:type] == :missing_from_base
22
- locale = magenta bold key.locale
22
+ locale = magenta key.locale
23
23
  base_value = ''
24
24
  else
25
25
  locale = magenta key.locale
@@ -28,7 +28,7 @@ module I18n
28
28
  [{value: locale, alignment: :center},
29
29
  {value: glyph, alignment: :center},
30
30
  magenta(key[:key]),
31
- base_value]
31
+ cyan(base_value)]
32
32
  }
33
33
  end
34
34
  else
@@ -41,8 +41,8 @@ module I18n
41
41
  keys.sort_by_attr!(key: :asc)
42
42
  if keys.present?
43
43
  keys.each do |k|
44
- puts "#{bold "#{k.key}"} #{green k[:usages].size if k[:usages].size > 1}"
45
- k[:usages].each do |u|
44
+ puts "#{bold "#{k.key}"} #{green(k.usages.size.to_s) if k.usages.size > 1}"
45
+ k.usages.each do |u|
46
46
  line = u[:line].dup.tap { |line|
47
47
  line.strip!
48
48
  line.sub!(/(.*?)(#{k[:key]})(.*)$/) { dark($1) + underline($2) + dark($3)}
@@ -58,7 +58,7 @@ module I18n
58
58
  def unused_keys(keys = task.unused_keys)
59
59
  print_title unused_title(keys)
60
60
  if keys.present?
61
- print_table headings: [bold(magenta('i18n Key')), cyan("Base value (#{base_locale})")] do |t|
61
+ print_table headings: [bold(magenta('i18n Key')), bold(cyan("Base value (#{base_locale})"))] do |t|
62
62
  t.rows = keys.map { |k| [magenta(k.key), cyan(k.value)] }
63
63
  end
64
64
  else
@@ -5,17 +5,22 @@ module I18n::Tasks::Scanners
5
5
  include ::I18n::Tasks::KeyPatternMatching
6
6
  attr_reader :config, :key_filter, :record_usages
7
7
 
8
- def initialize(config)
8
+ def initialize(config = {})
9
9
  @config = config.dup.with_indifferent_access.tap do |conf|
10
10
  conf[:paths] = %w(app/) if conf[:paths].blank?
11
11
  conf[:include] = Array(conf[:include]) if conf[:include].present?
12
- conf[:exclude] = Array(conf[:exclude])
12
+ if conf.key?(:exclude)
13
+ conf[:exclude] = Array(conf[:exclude])
14
+ else
15
+ # exclude common binary extensions by default (images and fonts)
16
+ conf[:exclude] = %w(*.jpg *.png *.gif *.svg *.ico *.eot *.ttf *.woff)
17
+ end
13
18
  end
14
19
  @record_usages = false
15
20
  end
16
21
 
17
22
  def key_filter=(value)
18
- @key_filter = value
23
+ @key_filter = value
19
24
  @key_filter_pattern = compile_key_pattern(value) if @key_filter
20
25
  end
21
26
 
@@ -24,15 +29,16 @@ module I18n::Tasks::Scanners
24
29
  if @record_usages
25
30
  keys_with_usages
26
31
  else
27
- @keys ||= traverse_files { |path| scan_file(path, read_file(path)).map(&:key) }.reduce(:+).uniq
32
+ @keys ||= (traverse_files { |path| scan_file(path, read_file(path)).map(&:key) }.reduce(:+) || []).uniq
28
33
  end
29
34
  end
30
35
 
31
36
  def keys_with_usages
32
37
  with_usages do
33
- traverse_files { |path|
38
+ keys = traverse_files { |path|
34
39
  ::I18n::Tasks::KeyGroup.new(scan_file(path, read_file(path)), src_path: path)
35
- }.map(&:keys).reduce(:+).group_by(&:key).map { |key, key_usages|
40
+ }.map(&:keys).reduce(:+) || []
41
+ keys.group_by(&:key).map { |key, key_usages|
36
42
  {key: key, usages: key_usages.map { |usage| usage[:src].merge(path: usage[:src_path]) }}
37
43
  }
38
44
  end
@@ -53,7 +59,12 @@ module I18n::Tasks::Scanners
53
59
  # @return [Array] Results of block calls
54
60
  def traverse_files
55
61
  result = []
56
- Find.find(*config[:paths]) do |path|
62
+ paths = config[:paths].select { |p| File.exists?(p) }
63
+ if paths.empty?
64
+ STDERR.puts Term::ANSIColor.yellow("i18n-tasks: [WARN] search.paths (#{config[:paths]}) do not exist")
65
+ return result
66
+ end
67
+ Find.find(*paths) do |path|
57
68
  next if File.directory?(path) ||
58
69
  config[:include] && !path_fnmatch_any?(path, config[:include]) ||
59
70
  path_fnmatch_any?(path, config[:exclude])
@@ -65,6 +76,7 @@ module I18n::Tasks::Scanners
65
76
  def path_fnmatch_any?(path, globs)
66
77
  globs.any? { |glob| File.fnmatch(glob, path) }
67
78
  end
79
+
68
80
  protected :path_fnmatch_any?
69
81
 
70
82
  def with_key_filter(key_filter = nil)
@@ -97,16 +109,10 @@ module I18n::Tasks::Scanners
97
109
  }}
98
110
  end
99
111
 
100
- def extract_key_from_match(match, path)
101
- key = strip_literal(match[0])
102
- key = absolutize_key(key, path) if path && key.start_with?('.')
103
- key
104
- end
105
-
106
112
  # remove the leading colon and unwrap quotes from the key match
107
113
  def strip_literal(literal)
108
114
  key = literal
109
- key.slice!(0) if ':' == key[0]
115
+ key = key[1..-1] if ':' == key[0]
110
116
  key = key[1..-2] if %w(' ").include?(key[0])
111
117
  key
112
118
  end