i18n-tasks 0.2.22 → 0.3.0.rc1

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