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
@@ -4,26 +4,52 @@ module I18n::Tasks::Scanners
4
4
  # Scans for I18n.t usages
5
5
  #
6
6
  class PatternScanner < BaseScanner
7
- LITERAL_RE = /:?".+?"|:?'.+?'|:\w+/
8
- DEFAULT_PATTERN = /\bt(?:ranslate)?[( ]\s*(#{LITERAL_RE})/
9
-
10
7
  # Extract i18n keys from file based on the pattern which must capture the key literal.
11
8
  # @return [String] keys found in file
12
9
  def scan_file(path, text = read_file(path))
13
10
  keys = []
14
11
  text.scan(pattern) do |match|
15
12
  src_pos = Regexp.last_match.offset(0).first
16
- key = extract_key_from_match(match, path)
13
+ key = match_to_key(match, path)
17
14
  next unless valid_key?(key)
18
15
  keys << ::I18n::Tasks::Key.new(key, usage_context(text, src_pos))
19
16
  end
20
17
  keys
21
18
  end
22
19
 
20
+ def default_pattern
21
+ # capture only the first argument
22
+ /
23
+ #{translate_call_re} [\( ] \s* (?# fn call begin )
24
+ (#{literal_re}) (?# capture the first argument)
25
+ /x
26
+ end
27
+
23
28
  protected
24
29
 
30
+ # Given
31
+ # @param [MatchData] match
32
+ # @param [String] path
33
+ # @return [String] full absolute key name
34
+ def match_to_key(match, path)
35
+ key = strip_literal(match[0])
36
+ key = absolutize_key(key, path) if path && key.start_with?('.')
37
+ key
38
+ end
39
+
25
40
  def pattern
26
- @pattern ||= config[:pattern].present? ? Regexp.new(config[:pattern]) : DEFAULT_PATTERN
41
+ @pattern ||= config[:pattern].present? ? Regexp.new(config[:pattern]) : default_pattern
42
+ end
43
+
44
+ def translate_call_re
45
+ /\bt(?:ranslate)?/
46
+ end
47
+
48
+ # Match literals:
49
+ # * String: '', "#{}"
50
+ # * Symbol: :sym, :'', :"#{}"
51
+ def literal_re
52
+ /:?".+?"|:?'.+?'|:\w+/
27
53
  end
28
54
  end
29
55
  end
@@ -0,0 +1,75 @@
1
+ require 'i18n/tasks/scanners/pattern_scanner'
2
+
3
+ module I18n::Tasks::Scanners
4
+ # Scans for I18n.t(key, scope: ...) usages
5
+ # both scope: "literal", and scope: [:array, :of, 'literals'] forms are supported
6
+ # Caveat: scope is only detected when it is the first argument
7
+ class PatternWithScopeScanner < PatternScanner
8
+
9
+ def default_pattern
10
+ # capture the first argument and scope argument if present
11
+ /#{super}
12
+ (?: \s*,\s* #{scope_arg_re} )? (?# capture scope in second argument )
13
+ /x
14
+ end
15
+
16
+ protected
17
+
18
+ # Given
19
+ # @param [MatchData] match
20
+ # @param [String] path
21
+ # @return [String] full absolute key name with scope resolved if any
22
+ def match_to_key(match, path)
23
+ key = super
24
+ scope = match[1]
25
+ if scope
26
+ scope_ns = scope.gsub(/[\[\]\s]+/, '').split(',').map { |arg| strip_literal(arg) } * '.'
27
+ "#{scope_ns}.#{key}"
28
+ else
29
+ key unless match[0] =~ /\A\w/
30
+ end
31
+ end
32
+
33
+
34
+ # also parse expressions with literals
35
+ def literal_re
36
+ /(?: (?: #{super} ) | #{expr_re} )/x
37
+ end
38
+
39
+ # strip literals, convert expressions to #{interpolations}
40
+ def strip_literal(val)
41
+ if val =~ /\A\w/
42
+ "\#{#{val}}"
43
+ else
44
+ super(val)
45
+ end
46
+ end
47
+
48
+ # Regexps:
49
+
50
+ # scope: literal or code expression or an array of these
51
+ def scope_arg_re
52
+ /(?:
53
+ :#{scope_arg_name}\s*=>\s* | (?# :scope => :home )
54
+ #{scope_arg_name}:\s* (?# scope: :home )
55
+ ) (#{array_or_one_literal_re})/x
56
+ end
57
+
58
+ def scope_arg_name
59
+ 'scope'
60
+ end
61
+
62
+ # match code expression
63
+ # matches characters until , as a heuristic to parse scopes like [:categories, cat.key]
64
+ # can be massively improved by matching parenthesis
65
+ def expr_re
66
+ /[\w():"'.\s]+/x
67
+ end
68
+
69
+ # match +el+ or array of +el+
70
+ def array_or_one_literal_re(el = literal_re)
71
+ /#{el} |
72
+ \[\s* (?:#{el}\s*,\s*)* #{el} \s*\]/x
73
+ end
74
+ end
75
+ end
@@ -13,17 +13,7 @@ module I18n::Tasks::TranslationData
13
13
 
14
14
  # whether the value for key exists in locale (defaults: base_locale)
15
15
  def key_value?(key, locale = base_locale)
16
- t(data[locale], key).present?
17
- end
18
-
19
- # @return [Array<String>] all available locales
20
- def locales
21
- config[:locales] ||= I18n.available_locales.map(&:to_s)
22
- end
23
-
24
- # @return [String] default i18n locale
25
- def base_locale
26
- config[:base_locale] ||= I18n.default_locale.to_s
16
+ t(key, locale).present?
27
17
  end
28
18
 
29
19
  def non_base_locales(from = nil)
@@ -39,4 +29,35 @@ module I18n::Tasks::TranslationData
39
29
  data[target_locale] = data[target_locale]
40
30
  end
41
31
  end
32
+
33
+ # @param locales
34
+ # @param locale
35
+ # @param keys
36
+ # @param value
37
+ # @param values
38
+ def update_data(opts = {})
39
+ if opts.key?(:locales)
40
+ locales = (Array(opts[:locales]).presence || self.locales).map(&:to_s)
41
+ # make sure base_locale always comes first if present
42
+ locales = [base_locale] + (locales - [base_locale]) if locales.include?(base_locale)
43
+ opts = opts.except(:locales)
44
+ locales.each { |locale| update_data(opts.merge(locale: locale)) }
45
+ return
46
+ end
47
+ locale = opts[:locale] || base_locale
48
+ keys = opts[:keys]
49
+ keys = keys.call(locale) if keys.respond_to?(:call)
50
+ return if keys.empty?
51
+ values = opts[:values]
52
+ values = values.call(keys, locale) if values.respond_to?(:call)
53
+ unless values
54
+ value = opts[:value]
55
+ values = if value.respond_to?(:call)
56
+ keys.map { |key| value.call(key, locale) }
57
+ else
58
+ [value] * keys.size
59
+ end
60
+ end
61
+ data[locale] = data[locale].deep_merge(list_to_tree keys.map(&:to_s).zip(values))
62
+ end
42
63
  end
@@ -9,13 +9,14 @@ module I18n
9
9
  @unused_keys ||= {}
10
10
  @unused_keys[locale] ||= ::I18n::Tasks::KeyGroup.new(
11
11
  traverse_map_if(data[locale]) { |key, value|
12
- next if pattern_key?(key) || ignore_key?(key, :unused)
12
+ next if used_in_expr?(key) || ignore_key?(key, :unused)
13
13
  key = depluralize_key(locale, key)
14
14
  [key, value] unless used_key?(key)
15
15
  }.uniq, locale: locale, type: :unused)
16
16
  end
17
17
 
18
- def remove_unused!(locales = self.locales)
18
+ def remove_unused!(locales = nil)
19
+ locales ||= self.locales
19
20
  unused = unused_keys
20
21
  locales.each do |locale|
21
22
  data[locale] = list_to_tree traverse_map_if(data[locale]) { |key, value|
@@ -1,21 +1,25 @@
1
1
  require 'find'
2
- require 'i18n/tasks/scanners/pattern_scanner'
2
+ require 'i18n/tasks/scanners/pattern_with_scope_scanner'
3
3
 
4
4
  module I18n::Tasks::UsedKeys
5
5
  # find all keys in the source (relative keys are absolutized)
6
6
  # @return [Array<String>]
7
7
  def used_keys(with_usages = false)
8
8
  if with_usages
9
- I18n::Tasks::KeyGroup.new(scanner.keys_with_usages, type: :used, key_filter: scanner.key_filter)
9
+ used_keys_group scanner.keys_with_usages
10
10
  else
11
- @used_keys ||= I18n::Tasks::KeyGroup.new(scanner.keys, type: :used, key_filter: scanner.key_filter)
11
+ @used_keys ||= used_keys_group scanner.keys
12
12
  end
13
13
  end
14
14
 
15
+ def used_keys_group(keys, opts = {})
16
+ ::I18n::Tasks::KeyGroup.new(keys, {type: :used, key_filter: scanner.key_filter}.merge(opts))
17
+ end
18
+
15
19
  def scanner
16
20
  @scanner ||= begin
17
21
  search_config = (config[:search] || {}).with_indifferent_access
18
- class_name = search_config[:scanner] || '::I18n::Tasks::Scanners::PatternScanner'
22
+ class_name = search_config[:scanner] || '::I18n::Tasks::Scanners::PatternWithScopeScanner'
19
23
  class_name.constantize.new search_config.merge(relative_roots: relative_roots)
20
24
  end
21
25
  end
@@ -25,15 +29,14 @@ module I18n::Tasks::UsedKeys
25
29
  used_keys.include?(key)
26
30
  end
27
31
 
28
- # dynamically generated keys in the source, e.g t("category.#{category_key}")
29
- def pattern_key?(key)
30
- @pattern_keys_re ||= compile_start_with_re(pattern_key_prefixes)
31
- !!(key =~ @pattern_keys_re)
32
+ # @return whether the key is potentially used in a code expression such as:
33
+ # t("category.#{category_key}")
34
+ def used_in_expr?(key)
35
+ !!(key =~ expr_key_re)
32
36
  end
33
37
 
34
38
  # keys in the source that end with a ., e.g. t("category.#{cat.i18n_key}") or t("category." + category.key)
35
- def pattern_key_prefixes
36
- @pattern_keys_prefixes ||=
37
- used_keys.key_names.select { |k| k =~ /\#{.*?}/ || k.ends_with?('.') }.map { |k| k.split(/\.?#/)[0].presence }.compact
39
+ def expr_key_re
40
+ @expr_key_re ||= compile_key_pattern "{#{used_keys.keys.select(&:expr?).map(&:key_match_pattern) * ','}}"
38
41
  end
39
42
  end
@@ -1,5 +1,5 @@
1
1
  module I18n
2
2
  module Tasks
3
- VERSION = '0.2.22'
3
+ VERSION = '0.3.0.rc1'
4
4
  end
5
5
  end
@@ -1,130 +1,79 @@
1
- require 'set'
2
1
  require 'i18n/tasks'
3
- require 'i18n/tasks/reports/terminal'
4
- require 'active_support/core_ext/module/delegation'
5
- require 'i18n/tasks/reports/spreadsheet'
2
+ require 'i18n/tasks/commands'
6
3
 
7
- namespace :i18n do
8
- require 'highline/import'
4
+ cmd = I18n::Tasks::Commands.new
9
5
 
6
+ namespace :i18n do
10
7
  task :setup do
11
8
  end
12
9
 
13
- desc 'show missing translations'
10
+ desc cmd.desc :missing
14
11
  task :missing, [:locales] => 'i18n:setup' do |t, args|
15
- i18n_report.missing_keys i18n_task.missing_keys(locales: i18n_parse_locales(args[:locales]))
12
+ cmd.missing locales: args[:locales]
16
13
  end
17
14
 
18
15
  namespace :missing do
19
16
  desc 'keys present in code but not existing in base locale data'
20
- task :missing_from_base => 'i18n:setup' do |t, args|
21
- i18n_report.missing_keys i18n_task.keys_missing_from_base
17
+ task :missing_from_base => 'i18n:setup' do
18
+ cmd.missing type: :missing_from_base
22
19
  end
23
20
 
24
21
  desc 'keys present but with value same as in base locale'
25
22
  task :eq_base, [:locales] => 'i18n:setup' do |t, args|
26
- i18n_report.missing_keys i18n_task.missing_keys(type: :eq_base, locales: i18n_parse_locales(args[:locales]))
23
+ cmd.missing type: :eq_base, locales: args[:locales]
27
24
  end
28
25
 
29
26
  desc 'keys that exist in base locale but are blank in passed locales'
30
27
  task :missing_from_locale, [:locales] => 'i18n:setup' do |t, args|
31
- i18n_report.missing_keys i18n_task.missing_keys(type: :missing_from_locale, locales: i18n_parse_locales(args[:locales]))
28
+ cmd.missing type: :missing_from_locale, locales: args[:locales]
32
29
  end
33
30
  end
34
31
 
35
- desc 'show unused translations'
32
+ desc cmd.desc :show_unused
36
33
  task :unused => 'i18n:setup' do
37
- i18n_report.unused_keys
38
- end
39
-
40
- desc 'add placeholder for missing values to the base locale (default: key.humanize)'
41
- task :add_missing, [:placeholder] => 'i18n:setup' do |t, args|
42
- i18n_task.add_missing! base_locale, args[:placeholder]
34
+ cmd.unused
43
35
  end
44
36
 
45
- desc 'remove unused keys'
37
+ desc cmd.desc :remove_unused
46
38
  task :remove_unused, [:locales] => 'i18n:setup' do |t, args|
47
- locales = i18n_parse_locales(args[:locales]) || i18n_task.locales
48
- unused_keys = i18n_task.unused_keys
49
- if unused_keys.present?
50
- i18n_report.unused_keys(unused_keys)
51
- unless ENV['CONFIRM']
52
- exit 1 unless agree(red "All these translations will be removed in #{bold locales * ', '}#{red '.'} " + yellow('Continue? (yes/no)') + ' ')
53
- end
54
- i18n_task.remove_unused!(locales)
55
- else
56
- STDERR.puts bold green 'No unused keys to remove'
57
- end
39
+ cmd.remove_unused
58
40
  end
59
-
60
- desc 'show usages of the keys in the codebase'
41
+
42
+ desc cmd.desc :usages
61
43
  task :usages, [:filter] => 'i18n:setup' do |t, args|
62
- filter = args[:filter] ? args[:filter].tr('+', ',') : nil
63
- i18n_report.used_keys(
64
- i18n_task.scanner.with_key_filter(filter) {
65
- i18n_task.used_keys(true)
66
- }
67
- )
44
+ cmd.find filter: args[:filter]
68
45
  end
69
46
 
70
- desc 'normalize translation data: sort and move to the right files'
47
+ desc cmd.desc :normalize
71
48
  task :normalize, [:locales] => 'i18n:setup' do |t, args|
72
- i18n_task.normalize_store! args[:locales]
49
+ cmd.normalize locales: args[:locales]
73
50
  end
74
51
 
75
- desc 'save missing and unused translations to an Excel file'
76
- task :spreadsheet_report, [:path] => 'i18n:setup' do |t, args|
77
- begin
78
- require 'axlsx'
79
- rescue LoadError
80
- message = %Q(To use i18n:spreadsheet_report please add axlsx gem to Gemfile:\ngem 'axlsx', '~> 2.0')
81
- STDERR.puts Term::ANSIColor.red Term::ANSIColor.bold message
82
- exit 1
83
- end
84
- args.with_defaults path: 'tmp/i18n-report.xlsx'
85
- i18n_spreadsheet_report.save_report(args[:path])
86
- end
87
-
88
- desc 'fill translations with values'
89
- namespace :fill do
90
-
91
- desc 'add "" values for missing and untranslated keys to locales (default: all)'
92
- task :blanks, [:locales] => 'i18n:setup' do |t, args|
93
- i18n_task.fill_with_blanks! i18n_parse_locales args[:locales]
94
- end
95
-
52
+ namespace :add_missing do
96
53
  desc 'add Google Translated values for untranslated keys to locales (default: all non-base)'
97
- task :google_translate, [:locales] => 'i18n:setup' do |t, args|
98
- i18n_task.fill_with_google_translate! i18n_parse_locales args[:locales]
54
+ task :translate, [:locales] => 'i18n:setup' do |t, args|
55
+ cmd.translate locales: args[:locales]
99
56
  end
100
57
 
101
58
  desc 'copy base locale values for all untranslated keys to locales (default: all non-base)'
102
- task :base_value, [:locales] => 'i18n:setup' do |t, args|
103
- i18n_task.fill_with_base_values! i18n_parse_locales args[:locales]
104
- end
105
- end
106
-
107
- module ::I18n::Tasks::RakeHelpers
108
- include Term::ANSIColor
109
-
110
- delegate :base_locale, to: :i18n_task
111
-
112
- def i18n_task
113
- @i18n_task ||= I18n::Tasks::BaseTask.new
59
+ task :placeholder, [:locales, :placeholder] => 'i18n:setup' do |t, args|
60
+ cmd.add_missing locale: args[:locales], placeholder: args[:placeholder]
114
61
  end
115
62
 
116
- def i18n_report
117
- @i18n_report ||= I18n::Tasks::Reports::Terminal.new
63
+ desc 'add values for missing and untranslated keys to locales (default: all)'
64
+ task :empty_string, [:locales] => 'i18n:setup' do |t, args|
65
+ cmd.add_missing locales: args[:locales], placeholder: ''
118
66
  end
67
+ end
119
68
 
120
- def i18n_spreadsheet_report
121
- @i18n_spreadsheet_report ||= I18n::Tasks::Reports::Spreadsheet.new
122
- end
69
+ desc cmd.desc(:config)
70
+ task :tasks_config => 'i18n:setup' do
71
+ cmd.config
72
+ end
123
73
 
124
- def i18n_parse_locales(arg = nil)
125
- arg.try(:strip).try(:split, /\s*\+\s*/).try(:compact).try(:presence)
126
- end
74
+ desc cmd.desc :save_spreadsheet
75
+ task :spreadsheet_report, [:path] => 'i18n:setup' do |t, args|
76
+ cmd.xlsx_report path: args[:path]
127
77
  end
128
- include ::I18n::Tasks::RakeHelpers
129
78
  end
130
79
 
@@ -1,9 +1,29 @@
1
1
  class EventsController < ApplicationController
2
2
  def show
3
3
  redirect_to :edit, notice: I18n.t('cb.a')
4
+
5
+ # args are ignored
4
6
  I18n.t("cb.b", i: "Hello")
5
- I18n.t("hash_pattern.#{some_value}", i: "Hello")
6
- I18n.t("hash_pattern2." + some_value, i: "Hello")
7
- I18n.t "hash_pattern3", scope: "foo.bar"
7
+
8
+ # pattern not reported as unused
9
+ I18n.t("hash.pattern.#{some_value}", i: "Hello")
10
+
11
+ # pattern also not reported as unused
12
+ I18n.t("hash.pattern2." + some_value, i: "Hello")
13
+
14
+ # same as above but with scope argument
15
+ I18n.t(some_value, scope: [:hash, :pattern3])
16
+
17
+ # missing:
18
+ I18n.t 'pattern_missing.a', scope: :hash, other: 1
19
+
20
+ # missing:
21
+ I18n.t :b, scope: [:hash, :pattern_missing], other: 1
22
+
23
+ # missing, but not yet detected as such :(
24
+ I18n.t "#{stuff}.pattern_missing.c"
25
+
26
+ # not missing
27
+ I18n.t "hash.#{stuff}.a"
8
28
  end
9
29
  end