i18n-tasks 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f116de755ba0474543cb353a777bf20e7eeab5ac
4
- data.tar.gz: 2850693394e00b2c1aad7421695aa4d42d4c4f45
3
+ metadata.gz: 9c0488a6ce907e9327da7ed07ffea723e8f8010f
4
+ data.tar.gz: 8f5042c6c785f81d5c75bc953fcd7d2cce496870
5
5
  SHA512:
6
- metadata.gz: 85aa544fd0af018aade6cf796efbfbd20028a0f296b06f48e4f4c9941fa1c79862ecd5a31c9f8aa34d0abc9908d3dd6c1a56a696557e99a10d55866a4c3d4363
7
- data.tar.gz: cc88b01eb13bbe1c00e3d3ef89f2ec239f4b673a124cb436dc612ced9c318bd977baa32bc8daf50ae62fcfe4b7161afa686bc4225484a4598f380e7f4fded9bf
6
+ metadata.gz: 9297eb48c4d50a91e4825b2a3867360f0296f780bfecb7fd9e6ae39b98501ca916557f3ab12501be96ab45eccfbad46628d315b297edf761887d6c9471a98579
7
+ data.tar.gz: 6eebe2d12de95e7e8563409007dd2df906768e7860047370d3e5cd9952b6382ac8abcb3096b14e404431c9477c5a54793e4a2068df956346d57536c4338008f2
data/.gitignore CHANGED
@@ -32,3 +32,4 @@ test/version_tmp
32
32
  tmp
33
33
  .ruby-version
34
34
  .ruby-gemset
35
+ *~
data/.travis.yml CHANGED
@@ -3,10 +3,12 @@ rvm:
3
3
  - 2.0.0
4
4
  - 1.9.3
5
5
  - jruby
6
+ - jruby-head
6
7
  - rbx-19mode
7
8
 
8
9
  matrix:
9
10
  allow_failures:
10
11
  - rvm: jruby
12
+ - rvm: jruby-head
11
13
  - rvm: rbx-19mode
12
14
 
data/CHANGES.md ADDED
@@ -0,0 +1,4 @@
1
+ ## v0.1.2
2
+
3
+ * added grep config options (thanks @dmke)
4
+ * improved terminal output
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
- i18n-tasks [![Build Status](https://travis-ci.org/glebm/i18n-tasks.png?branch=master)](https://travis-ci.org/glebm/i18n-tasks)
2
- ==========
1
+ # i18n-tasks [![Build Status](https://travis-ci.org/glebm/i18n-tasks.png?branch=master)](https://travis-ci.org/glebm/i18n-tasks) [![Code Climate](https://codeclimate.com/github/glebm/i18n-tasks.png)](https://codeclimate.com/github/glebm/i18n-tasks)
2
+
3
3
 
4
4
  Rails I18n tasks to find missing / unused translations and more. Works with slim / coffee / haml etc.
5
5
 
6
- ![i18n-missing-screenshot]
6
+ ![i18n-missing-screenshot](https://raw.github.com/glebm/i18n-tasks/master/doc/img/i18n-tasks.png "rake i18n:missing output screenshot")
7
7
 
8
8
  Use `rake -T i18n` to get the list of tasks with descriptions. There are 3 tasks available at the moment:
9
9
 
10
- * `i18n:missing` task shows all the keys that have not been translated yet *([source](https://github.com/glebm/i18n-tasks/blob/master/lib/i18n/tasks/missing.rb))*
11
- * `i18n:prefill` task normalizes locale files, and adds missing keys from base locale to others *([source](https://github.com/glebm/i18n-tasks/blob/master/lib/i18n/tasks/prefill.rb))*
12
- * `i18n:unused` task shows potentially unused translations *([source](https://github.com/glebm/i18n-tasks/blob/master/lib/i18n/tasks/unused.rb))*
10
+ * `i18n:missing` task shows all the keys that have not been translated yet *([source](./blob/master/lib/i18n/tasks/missing.rb))*
11
+ * `i18n:prefill` task normalizes locale files, and adds missing keys from base locale to others *([source](./blob/master/lib/i18n/tasks/prefill.rb))*
12
+ * `i18n:unused` task shows potentially unused translations *([source](./blob/master/lib/i18n/tasks/unused.rb))*
13
13
 
14
- `i18n:unused` will detect pattern translations and not report them, e.g.:
14
+ The `i18n:unused` task will detect pattern translations and not report them, e.g.:
15
15
 
16
16
  ```ruby
17
17
  t 'category.' + category.key # 'category.arts_and_crafts' considered used
@@ -20,10 +20,10 @@ t "category.#{category.key}" # also works
20
20
 
21
21
  Relative keys (`t '.title'`) are supported too.
22
22
 
23
- For more examples see [the tests](https://github.com/glebm/i18n-tasks/blob/master/spec/i18n_tasks_spec.rb#L43-L59).
23
+ For more examples see [the tests](./blob/master/spec/i18n_tasks_spec.rb#L43-L59).
24
+
24
25
 
25
- Installation
26
- ------------
26
+ ## Installation
27
27
 
28
28
  Simply add to Gemfile:
29
29
 
@@ -33,9 +33,8 @@ gem 'i18n-tasks', '~> 0.1.0'
33
33
 
34
34
  `grep` is required. You likely have it already on Linux / Mac / BSD, Windows users will need to [install](http://gnuwin32.sourceforge.net/packages/grep.htm) and make sure it's available in `PATH`.
35
35
 
36
- Configuration
37
- -------------
38
36
 
37
+ ## Configuration
39
38
 
40
39
  Tasks may incorrectly report framework i18n keys as missing. You can add `config/i18n-tasks.yml` to work around this:
41
40
 
@@ -59,6 +58,19 @@ ignore_unused:
59
58
  # do not report these keys ever
60
59
  ignore:
61
60
  - kaminari.
61
+
62
+ # grep configuration
63
+ grep:
64
+ # search these directories (relative to your Rails.root directory, default: 'app/')
65
+ paths:
66
+ - 'app/'
67
+ - 'vendor/'
68
+ # include only files matching this glob pattern (default: blank = include all files)
69
+ include:
70
+ - '*.rb'
71
+ - '*.html*'
72
+ # explicitly exclude files (default: blank = exclude no files)
73
+ exclude: '*.js'
62
74
  ```
63
75
 
64
76
 
@@ -75,8 +87,11 @@ I18n::Tasks.get_locale_data = ->(locale) {
75
87
  }
76
88
  ```
77
89
 
90
+ ## i18n-tasks HTML report
91
+
92
+ While i18n-tasks does not provide an HTML version of the report, it's easy to roll your own, see [the example](https://gist.github.com/glebm/6887030).
93
+
78
94
  ---
79
95
 
80
96
  This was originally developed for [Zuigo](http://zuigo.com/), a platform to organize and discover events.
81
97
 
82
- [i18n-missing-screenshot]: https://raw.github.com/glebm/i18n-tasks/master/doc/img/i18n-missing.png "rake i18n:missing output screenshot"
Binary file
data/i18n-tasks.gemspec CHANGED
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency 'bundler', '~> 1.3'
25
25
  spec.add_development_dependency 'rake'
26
26
  spec.add_development_dependency 'rspec-rails'
27
+ spec.add_development_dependency 'yard'
27
28
  end
@@ -5,78 +5,125 @@ require 'i18n/tasks/task_helpers'
5
5
  module I18n
6
6
  module Tasks
7
7
  class BaseTask
8
- include Term::ANSIColor
9
8
  include TaskHelpers
10
9
 
11
10
  # locale data hash, with locale name as root
11
+ # @return [Hash{String => String,Hash}] locale data in nested hash format
12
12
  def get_locale_data(locale)
13
13
  (@locale_data ||= {})[locale] ||= I18n::Tasks.get_locale_data.call(locale)
14
14
  end
15
15
 
16
16
  # main locale file path (for writing to)
17
+ # @return [String]
17
18
  def locale_file_path(locale)
18
19
  "config/locales/#{locale}.yml"
19
20
  end
20
21
 
21
22
  # find all keys in the source (relative keys are returned in absolutized)
23
+ # @return [Array<String>]
22
24
  def find_source_keys
23
25
  @source_keys ||= begin
24
- grep_out = run_command 'grep', '-HorI', %q{\\bt(\\?\\s*['"]\\([^'"]*\\)['"]}, 'app/'
25
- if grep_out
26
- used_keys = grep_out.split("\n").map { |r|
26
+ if (grep_out = run_grep)
27
+ grep_out.split("\n").map { |r|
27
28
  key = r.match(/['"](.*?)['"]/)[1]
28
- # absolutize relative key:
29
29
  if key.start_with? '.'
30
- path = r.split(':')[0]
31
- # normalized path
32
- path = Pathname.new(File.expand_path path).relative_path_from(Pathname.new(Dir.pwd)).to_s
33
- # key prefix based on path
34
- prefix = path.gsub(%r(app/views/|(\.[^/]+)*$), '').tr('/', '.')
35
- "#{prefix}#{key}"
30
+ absolutize_key key, r.split(':')[0]
36
31
  else
37
32
  key
38
33
  end
39
- }.uniq
40
- used_keys.reject { |k| k !~ /^[\w.\#{}]+$/ }
34
+ }.uniq.reject { |k| k !~ /^[\w.\#{}]+$/ }
41
35
  else
42
36
  []
43
37
  end
44
38
  end
45
39
  end
46
40
 
41
+ # whether the key is used in the source
42
+ def used_key?(key)
43
+ @used_keys ||= find_source_keys.to_set
44
+ @used_keys.include?(key)
45
+ end
46
+
47
+ # whether to ignore the key. ignore_type one of :missing, :eq_base, :blank, :unused.
48
+ # will apply global ignore rules as well
49
+ def ignore_key?(key, ignore_type, locale = nil)
50
+ key =~ ignore_pattern(ignore_type, locale)
51
+ end
47
52
 
48
- def find_source_pattern_keys
49
- @source_pattern_keys ||= find_source_keys.select { |k| k =~ /\#{.*?}/ || k.ends_with?('.') }
53
+ # dynamically generated keys in the source, e.g t("category.#{category_key}")
54
+ def pattern_key?(key)
55
+ @pattern_keys_re ||= compile_start_with_re(pattern_key_prefixes)
56
+ key =~ @pattern_keys_re
50
57
  end
51
58
 
52
- def find_source_pattern_prefixes
53
- @source_pattern_prefixes ||= find_source_pattern_keys.map { |k| k.split(/\.?#/)[0] }
59
+ # keys in the source that end with a ., e.g. t("category.#{cat.i18n_key}") or t("category." + category.key)
60
+ def pattern_key_prefixes
61
+ @pattern_keys_prefixes ||=
62
+ find_source_keys.select { |k| k =~ /\#{.*?}/ || k.ends_with?('.') }.map { |k| k.split(/\.?#/)[0].presence }.compact
63
+ end
64
+
65
+ # whether the value for key exists in locale (defaults: base_locale)
66
+ def key_has_value?(key, locale = base_locale)
67
+ t(get_locale_data(locale)[locale], key).present?
54
68
  end
55
69
 
56
70
  # traverse hash, yielding with full key and value
71
+ # @param hash [Hash{String => String,Hash}] translation data to traverse
72
+ # @yield [full_key, value] yields full key and value for every translation in #hash
73
+ # @return [nil]
57
74
  def traverse(path = '', hash)
58
75
  q = [ [path, hash] ]
59
76
  until q.empty?
60
77
  path, value = q.pop
61
78
  if value.is_a?(Hash)
62
- value.each { |k, v| q << ["#{path}.#{k}", v] }
79
+ value.each { |k,v| q << ["#{path}.#{k}", v] }
63
80
  else
64
81
  yield path[1..-1], value
65
82
  end
66
83
  end
67
84
  end
68
85
 
86
+ # translation of the key found in the passed hash or nil
87
+ # @return [String,nil]
69
88
  def t(hash, key)
70
- key.split('.').inject(hash) { |r, seg| r[seg] if r }
89
+ key.split('.').inject(hash) { |r,seg| r[seg] if r }
90
+ end
91
+
92
+ # @param key [String] relative i18n key (starts with a .)
93
+ # @param path [String] path to the file containing the key
94
+ # @return [String] absolute version of the key
95
+ def absolutize_key(key, path)
96
+ # normalized path
97
+ path = Pathname.new(File.expand_path path).relative_path_from(Pathname.new(Dir.pwd)).to_s
98
+ # key prefix based on path
99
+ prefix = path.gsub(%r(app/views/|(\.[^/]+)*$), '').tr('/', '.')
100
+ "#{prefix}#{key}"
71
101
  end
72
102
 
103
+
104
+ # @return [String] default i18n locale
73
105
  def base_locale
74
106
  I18n.default_locale.to_s
75
107
  end
76
108
 
77
- def base
78
- @base ||= get_locale_data(base_locale)
109
+ # @return [Hash{String => String,Hash}] default i18n locale data
110
+ def base_locale_data
111
+ get_locale_data(base_locale)[base_locale]
112
+ end
113
+
114
+ # Run grep searching for source keys and return grep output
115
+ # @return [String] output of the grep command
116
+ def run_grep
117
+ args = ['grep', '-HoRI']
118
+ [:include, :exclude].each do |opt|
119
+ next unless (val = grep_config[opt]).present?
120
+ args += Array(val).map { |v| "--#{opt}=#{v}" }
121
+ end
122
+ args += [ %q{\\bt(\\?\\s*['"]\\([^'"]*\\)['"]}, *grep_config[:paths]]
123
+ args.compact!
124
+ run_command *args
79
125
  end
126
+
80
127
  end
81
128
  end
82
129
  end
@@ -4,73 +4,51 @@ require 'i18n/tasks/base_task'
4
4
  module I18n
5
5
  module Tasks
6
6
  class Missing < BaseTask
7
- DESC = 'Missing keys and translations'
8
7
 
9
- def perform
10
- missing = find_missing
11
- STDERR.puts bold cyan("= #{DESC} (#{missing.length}) =")
12
- STDERR.puts cyan(" This task may report framework i18n keys as missing (use .i18nignore if that happens)")
13
- STDERR.puts cyan " Legend:\t#{red '✗'} - key is missing\t#{yellow bold '∅'} - translation is blank\t#{yellow bold '='} - value same as base locale"
14
- status_texts = {
15
- none: red("✗".ljust(6)),
16
- blank: yellow(bold '∅'.ljust(6)),
17
- eq_base: yellow(bold "=".ljust(6))
18
- }
19
-
20
- missing.sort { |a, b| (l = a[:locale] <=> b[:locale]).zero? ? a[:type] <=> b[:type] : l }.each do |m|
21
- locale, key, base_value = m[:locale], m[:key], m[:base_value]
22
- status_text = ' ' + status_texts[m[:type]]
23
- case m[:type]
24
- when :none
25
- puts "#{red p_locale base_locale} #{status_text} #{p_key key}"
26
- when :blank
27
- puts "#{p_locale locale} #{status_text} #{p_key key} #{cyan base_value}"
28
- when :eq_base
29
- puts "#{p_locale locale} #{status_text} #{p_key key} #{cyan base_value}"
30
- end
31
- end
8
+ # Get all the missing translations as an array of missing keys as hashes with the following options:
9
+ # :locale
10
+ # :key
11
+ # :type :blank, :missing, or :eq_base
12
+ # :base_value translation value in base locale if one is present
13
+ # @return [Array<Hash{Symbol => String,Symbol,nil}>]
14
+ def find_keys
15
+ other_locales = I18n.available_locales.map(&:to_s) - [base_locale]
16
+ sort_keys keys_missing_base_value + other_locales.map { |locale| keys_missing_translation(locale) }.flatten(1)
32
17
  end
33
18
 
19
+ private
34
20
 
21
+ # missing keys, i.e. key that are in the code but are not in the base locale data
22
+ # @return Array{Hash}
23
+ def keys_missing_base_value
24
+ find_source_keys.reject { |key|
25
+ key_has_value?(key, base_locale) || pattern_key?(key) || ignore_key?(key, :missing)
26
+ }.map { |key| {locale: base_locale, type: :none, key: key} }
27
+ end
35
28
 
36
- # get all the missing translations as list of missing keys as hashes with:
37
- # {:locale, :key, :type, and optionally :base_value}
38
- # :type — :blank, :missing, or :eq_base
39
- # :base_value translation value in base locale if one is present
40
- def find_missing
41
- # dynamically generated keys in the source, e.g t("category.#{category_key}")
42
- pattern_re = compile_start_with_re find_source_pattern_prefixes
43
-
44
- # missing keys (in the code but not in base locale data)
45
- keys = find_source_keys
46
- missing = keys.select { |key| t(base[base_locale], key).blank? && key !~ pattern_re && key !~ ignore_pattern(:missing) }.map do |key|
47
- {locale: base_locale, type: :none, key: key}
48
- end
49
-
50
- # missing translations (present in base locale, but untranslated in another locale )
51
- (I18n.available_locales.map(&:to_s) - [base_locale]).each do |locale|
52
- trn = get_locale_data(locale)[locale]
53
- traverse base[base_locale] do |key, base_value|
54
- translated = t(trn, key)
55
- if translated.blank? && key !~ ignore_pattern(:missing)
56
- missing << {locale: locale, key: key, type: :blank, base_value: base_value}
57
- elsif translated == base_value && key !~ ignore_pattern(:eq_base, locale)
58
- missing << {locale: locale, key: key, type: :eq_base, base_value: base_value}
59
- end
29
+ # present in base locale, but untranslated in another locale
30
+ # @return Array{Hash}
31
+ def keys_missing_translation(locale)
32
+ trn = get_locale_data(locale)[locale]
33
+ r = []
34
+ traverse base_locale_data do |key, base_value|
35
+ value_in_locale = t(trn, key)
36
+ if value_in_locale.blank? && !ignore_key?(key, :missing)
37
+ r << {locale: locale, key: key, type: :blank, base_value: base_value}
38
+ elsif value_in_locale == base_value && !ignore_key?(key, :eq_base, locale)
39
+ r << {locale: locale, key: key, type: :eq_base, base_value: base_value}
60
40
  end
61
41
  end
62
-
63
- missing
64
- end
65
-
66
- def p_locale(locale)
67
- ' ' + bold(locale.ljust(5))
42
+ r
68
43
  end
69
44
 
70
- def p_key(key)
71
- magenta(key).ljust(50)
45
+ # sort first by locale, then by type
46
+ # @return Array{Hash}
47
+ def sort_keys(keys)
48
+ keys.sort { |a, b|
49
+ (l = a[:locale] <=> b[:locale]).zero? ? a[:type] <=> b[:type] : l
50
+ }
72
51
  end
73
-
74
52
  end
75
53
  end
76
54
  end
@@ -0,0 +1,44 @@
1
+ # coding: utf-8
2
+ module I18n
3
+ module Tasks
4
+ module Output
5
+ class Terminal
6
+ include Term::ANSIColor
7
+
8
+ def missing(missing)
9
+ $stderr.puts bold cyan "Missing keys and translations (#{missing.length})"
10
+ $stderr.puts "#{bold 'Legend:'} #{red '✗'} key missing, #{yellow bold '∅'} translation blank, #{blue bold '='} value equal to base locale; #{cyan 'value in base locale'}"
11
+ key_col_width = missing.map { |x| x[:key] }.max_by(&:length).length + 2
12
+ missing.each { |m| print_missing_translation m, key_col_width: key_col_width }
13
+ end
14
+
15
+ def unused(unused)
16
+ $stderr.puts bold cyan("Unused i18n keys (#{unused.length})")
17
+ key_col_width = unused.max_by { |x| x[0].length }[0].length + 2
18
+ unused.each { |(key, value)| puts "#{magenta key.ljust(key_col_width)}#{cyan value.strip}" }
19
+ end
20
+
21
+ private
22
+
23
+ extend Term::ANSIColor
24
+ STATUS_TEXTS = {
25
+ none: red("✗".ljust(6)),
26
+ blank: yellow(bold '∅'.ljust(6)),
27
+ eq_base: blue(bold "=".ljust(6))
28
+ }
29
+
30
+ def print_missing_translation(m, opts)
31
+ locale, key, base_value, status_text = m[:locale], m[:key], m[:base_value].try(:strip), " #{STATUS_TEXTS[m[:type]]}"
32
+
33
+ key = magenta key.ljust(opts[:key_col_width])
34
+ s = if m[:type] == :none
35
+ "#{red bold locale.ljust(4)} #{status_text} #{key}"
36
+ else
37
+ "#{bold locale.ljust(4)} #{status_text} #{key} #{cyan base_value}"
38
+ end
39
+ puts s
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -7,7 +7,7 @@ module I18n
7
7
  # Will also rewrite en, good for ordering
8
8
  I18n.available_locales.map(&:to_s).each do |target_locale|
9
9
  trn = get_locale_data(target_locale)
10
- prefilled = { target_locale => base[base_locale] }.deep_merge(trn)
10
+ prefilled = { target_locale => base_locale_data }.deep_merge(trn)
11
11
  File.open(locale_file_path(target_locale), 'w'){ |f| f.write prefilled.to_yaml }
12
12
  end
13
13
  end
@@ -1,15 +1,20 @@
1
1
  # coding: utf-8
2
2
  require 'open3'
3
+
3
4
  module I18n
4
5
  module Tasks
5
6
  module TaskHelpers
6
7
  # Run command and get only stdout output
8
+ # @return [String] output
9
+ # @raise [RuntimeError] if grep returns with exit code other than 0
7
10
  def run_command(*args)
8
- _in, out, _err = Open3.popen3(*args)
9
- out.gets nil
11
+ o, e, s = Open3.capture3(*args)
12
+ raise "#{args[0]} failed with status #{s.exitstatus} (stderr: #{e})" unless s.success?
13
+ o
10
14
  end
11
15
 
12
16
  # compile prefix matching Regexp from the list of prefixes
17
+ # @return [Regexp] regexp matching any of the prefixes
13
18
  def compile_start_with_re(prefixes)
14
19
  if prefixes.blank?
15
20
  /\Z\A/ # match nothing
@@ -18,30 +23,42 @@ module I18n
18
23
  end
19
24
  end
20
25
 
21
- # exclude @keys with prefixes matching @patterns
26
+ # @return [Array<String>] keys sans passed patterns
22
27
  def exclude_patterns(keys, patterns)
23
28
  pattern_re = compile_start_with_re patterns.select { |p| p.end_with?('.') }
24
29
  (keys - patterns).reject { |k| k =~ pattern_re }
25
30
  end
26
31
 
27
- # type: missing, eq_base, unused
32
+ # @param type [:missing, :eq_base, :unused] type
33
+ # @param locale [String] only when type is :eq_base
34
+ # @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
28
35
  def ignore_pattern(type, locale = nil)
29
36
  ((@ignore_patterns ||= HashWithIndifferentAccess.new)[type] ||= {})[locale] = begin
30
- global = config[:ignore] || []
31
- type_ignore = config["ignore_#{type}"] || []
37
+ global, type_ignore = config[:ignore].presence || [], config["ignore_#{type}"].presence || []
32
38
  if type_ignore.is_a?(Array)
33
- compile_start_with_re global + type_ignore
39
+ patterns = global + type_ignore
34
40
  elsif type_ignore.is_a?(Hash)
35
- p = global + (type_ignore[:all] || [])
36
- type_ignore.each { |key, value| p += (value || []) if key.to_s =~ /\b#{locale}\b/ } if locale
37
- compile_start_with_re p
41
+ # ignore per locale
42
+ patterns = global + (type_ignore[:all] || []) +
43
+ type_ignore.select { |k, v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
38
44
  end
45
+ compile_start_with_re patterns
39
46
  end
40
47
  end
41
48
 
49
+ # i18n-tasks config (defaults + config/i18n-tasks.yml)
50
+ # @return [Hash{String => String,Hash,Array}]
42
51
  def config
43
52
  I18n::Tasks.config
44
53
  end
54
+
55
+ # grep config, also from config/i18n-tasks.yml
56
+ # @return [Hash{String => String,Hash,Array}]
57
+ def grep_config
58
+ @grep_config ||= (config[:grep] || {}).with_indifferent_access.tap do |conf|
59
+ conf[:paths] = ['app/'] if conf[:paths].blank?
60
+ end
61
+ end
45
62
  end
46
63
  end
47
64
  end
@@ -4,24 +4,11 @@ require 'i18n/tasks/base_task'
4
4
  module I18n
5
5
  module Tasks
6
6
  class Unused < BaseTask
7
- DESC = 'Unused i18n keys'
8
- def perform
9
- unused = find_unused
10
- STDERR.puts bold cyan("= #{DESC} (#{unused.length}) =")
11
- unused.each do |(key, value)|
12
- puts " #{magenta(key).ljust(60)}\t#{cyan value}"
13
- end
14
- end
15
-
16
- def find_unused
17
- used_keys = find_source_keys.to_set
7
+ # @return [Array<[String, String]>] all the unused translations as an array of [key, value] pairs
8
+ def find_keys
18
9
  r = []
19
- ignore_re = ignore_pattern(:unused)
20
- pattern_re = compile_start_with_re find_source_pattern_prefixes
21
- traverse base[base_locale] do |key, value|
22
- unless used_keys.include?(key) || key =~ pattern_re || key =~ ignore_re
23
- r << [key, value]
24
- end
10
+ traverse base_locale_data do |key, value|
11
+ r << [key, value] unless used_key?(key) || pattern_key?(key) || ignore_key?(key, :unused)
25
12
  end
26
13
  r
27
14
  end
@@ -1,5 +1,5 @@
1
1
  module I18n
2
2
  module Tasks
3
- VERSION = '0.1.1'
3
+ VERSION = '0.1.2'
4
4
  end
5
5
  end
@@ -4,6 +4,7 @@ require 'active_support/core_ext'
4
4
  require 'i18n/tasks/missing'
5
5
  require 'i18n/tasks/prefill'
6
6
  require 'i18n/tasks/unused'
7
+ require 'i18n/tasks/output/terminal'
7
8
 
8
9
  namespace :i18n do
9
10
  desc 'add keys from base locale to others'
@@ -14,14 +15,14 @@ namespace :i18n do
14
15
  desc 'show keys with translation values identical to base'
15
16
  task :missing => :environment do
16
17
  if File.exists?('.i18nignore')
17
- STDERR.puts "!!!! Looks like you are using .i18ignore. It is no longer used in favour of config/i18n-tasks.yml.\n
18
- See README.md https://github.com/glebm/i18n-tasks"
18
+ STDERR.puts 'Looks like you are using .i18ignore. It is no longer used in favour of config/i18n-tasks.yml.'
19
+ STDERR.puts 'See README.md https://github.com/glebm/i18n-tasks'
19
20
  end
20
- I18n::Tasks::Missing.new.perform
21
+ I18n::Tasks::Output::Terminal.new.missing I18n::Tasks::Missing.new.find_keys
21
22
  end
22
23
 
23
24
  desc 'find potentially unused translations'
24
25
  task :unused => :environment do
25
- I18n::Tasks::Unused.new.perform
26
+ I18n::Tasks::Output::Terminal.new.unused I18n::Tasks::Unused.new.find_keys
26
27
  end
27
28
  end
@@ -0,0 +1,3 @@
1
+ //= require t
2
+
3
+ window.t = new Matrix.t(this)
@@ -0,0 +1,9 @@
1
+ class EventsController < ApplicationController
2
+ def show
3
+ redirect_to :edit, notice: I18n.t('cb.a')
4
+ 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"
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ p #{t('ca.a')} #{t 'ca.b'} #{t "ca.c"}
2
+ p #{t 'ca.d'} #{t 'ca.f', i: 'world'} #{t 'ca.e', i: 'world'}
3
+ p #{t 'missing_in_es.a'} #{t 'same_in_es.a'} #{t 'blank_in_es.a'}
4
+ p = t 'used_but_missing.a'
5
+ p = t 'ignored_missing_key.a'
6
+ p = t 'ignore.a'
7
+ p = t 'ignored_pattern.some_key'
8
+ p = t 'ignore_eq_base_all.a'
9
+ p = t 'ignore_eq_base_es.a'
@@ -0,0 +1 @@
1
+ p = t '.title'
@@ -0,0 +1,33 @@
1
+ # do not report these keys as missing:
2
+ ignore_missing:
3
+ - ignored_missing_key.a # one key to ignore
4
+ - ignored_pattern. # ignore the whole pattern
5
+
6
+ # do not report these keys when they have the same value as the base locale version
7
+ ignore_eq_base:
8
+ all:
9
+ - ignore_eq_base_all.a
10
+ es:
11
+ - ignore_eq_base_es.a
12
+
13
+ # do not report these keys as unused
14
+ ignore_unused:
15
+ - ignore_unused.a
16
+
17
+ # do not report these keys ever
18
+ ignore:
19
+ - ignore.a
20
+
21
+ # grep configuration
22
+ grep:
23
+ # search these directories (relative to your Rails.root directory, default: 'app/')
24
+ paths:
25
+ - 'app/'
26
+ - 'vendor/'
27
+ # include only files matching this glob pattern (default: blank = include all files)
28
+ include:
29
+ - '*.rb'
30
+ - '*.html.*'
31
+ - '*.file'
32
+ # explicitly exclude files (default: blank = exclude no files)
33
+ exclude: '*.js'
@@ -4,13 +4,17 @@ require 'spec_helper'
4
4
  describe 'rake i18n' do
5
5
  describe 'missing' do
6
6
  it 'detects missing or identical' do
7
- TestCodebase.rake_result('i18n:missing').should be_i18n_keys %w(en.used_but_missing.a es.missing_in_es.a es.blank_in_es.a es.same_in_es.a)
7
+ TestCodebase.capture_stderr do
8
+ TestCodebase.rake_result('i18n:missing').should be_i18n_keys %w(en.used_but_missing.a es.missing_in_es.a es.blank_in_es.a es.same_in_es.a)
9
+ end.should =~ /Missing keys and translations \(4\)/
8
10
  end
9
11
  end
10
12
 
11
13
  describe 'unused' do
12
14
  it 'detects unused' do
13
- TestCodebase.rake_result('i18n:unused').should be_i18n_keys %w(unused.a)
15
+ TestCodebase.capture_stderr do
16
+ TestCodebase.rake_result('i18n:unused').should be_i18n_keys %w(unused.a)
17
+ end.should =~ /Unused i18n keys \(1\)/
14
18
  end
15
19
  end
16
20
 
@@ -23,86 +27,46 @@ describe 'rake i18n' do
23
27
  end
24
28
 
25
29
  # --- setup ---
26
- BENCH_KEYS = 3000
30
+ BENCH_KEYS = 30
27
31
  before do
28
32
  gen_data = ->(v) {
29
33
  {
30
- 'ca' => {'a' => v, 'b' => v, 'c' => v, 'd' => v, 'e' => "#{v}%{i}", 'f' => "#{v}%{i}"},
31
- 'cb' => {'a' => v, 'b' => "#{v}%{i}"},
32
- 'hash_pattern' => {'a' => v},
33
- 'hash_pattern2' => {'a' => v},
34
- 'unused' => {'a' => v},
35
- 'ignore_unused' => {'a' => v},
36
- 'missing_in_es' => {'a' => v},
37
- 'same_in_es' => {'a' => v},
38
- 'ignore_eq_base_all' => {'a' => v},
39
- 'ignore_eq_base_es' => {'a' => v},
40
- 'blank_in_es' => {'a' => v},
41
- 'relative' => {'index' => {'title' => v}}
42
-
43
- }.tap {|r|
34
+ 'ca' => {'a' => v, 'b' => v, 'c' => v, 'd' => v, 'e' => "#{v}%{i}", 'f' => "#{v}%{i}"},
35
+ 'cb' => {'a' => v, 'b' => "#{v}%{i}"},
36
+ 'hash_pattern' => {'a' => v},
37
+ 'hash_pattern2' => {'a' => v},
38
+ 'unused' => {'a' => v},
39
+ 'ignore_unused' => {'a' => v},
40
+ 'missing_in_es' => {'a' => v},
41
+ 'same_in_es' => {'a' => v},
42
+ 'ignore_eq_base_all' => {'a' => v},
43
+ 'ignore_eq_base_es' => {'a' => v},
44
+ 'blank_in_es' => {'a' => v},
45
+ 'relative' => {'index' => {'title' => v}}
46
+ }.tap { |r|
44
47
  gen = r["bench"] = {}
45
- BENCH_KEYS.times.map { |i| gen["key#{i}"] = v }
48
+ BENCH_KEYS.times { |i| gen["key#{i}"] = v }
46
49
  }
47
50
  }
48
51
 
49
- en_data = gen_data.('EN_TEXT')
50
- es_data = gen_data.('ES_TEXT').except('missing_in_es')
52
+ en_data = gen_data.('EN_TEXT')
53
+ es_data = gen_data.('ES_TEXT').except('missing_in_es')
51
54
  es_data['same_in_es']['a'] = 'EN_TEXT'
52
55
  es_data['blank_in_es']['a'] = ''
53
56
  es_data['ignore_eq_base_all']['a'] = 'EN_TEXT'
54
57
  es_data['ignore_eq_base_es']['a'] = 'EN_TEXT'
55
58
 
56
59
  fs = {
57
- 'config/locales/en.yml' => {'en' => en_data}.to_yaml,
58
- 'config/locales/es.yml' => {'es' => es_data}.to_yaml,
59
- 'config/i18n-tasks.yml' => <<-YML,
60
- # do not report these keys as missing:
61
- ignore_missing:
62
- - ignored_missing_key.a # one key to ignore
63
- - ignored_pattern. # ignore the whole pattern
64
-
65
- # do not report these keys when they have the same value as the base locale version
66
- ignore_eq_base:
67
- all:
68
- - ignore_eq_base_all.a
69
- es:
70
- - ignore_eq_base_es.a
71
-
72
- # do not report these keys as unused
73
- ignore_unused:
74
- - ignore_unused.a
60
+ 'config/locales/en.yml' => {'en' => en_data}.to_yaml,
61
+ 'config/locales/es.yml' => {'es' => es_data}.to_yaml,
62
+ 'config/i18n-tasks.yml' => load_fixture('config/i18n-tasks.yml'),
63
+ 'app/views/index.html.slim' => load_fixture('app/views/index.html.slim'),
64
+ 'app/views/relative/index.html.slim' => load_fixture('app/views/relative/index.html.slim'),
65
+ 'app/controllers/events_controller.rb' => load_fixture('app/controllers/events_controller.rb'),
66
+ 'app/assets/javascripts/application.js' => load_fixture('app/assets/javascripts/application.js'),
75
67
 
76
- # do not report these keys ever
77
- ignore:
78
- - ignore.a
79
- YML
80
- 'app/views/index.html.slim' => <<-SLIM,
81
- p \#{t('ca.a')} \#{t 'ca.b'} \#{t "ca.c"}
82
- p \#{t 'ca.d'} \#{t 'ca.f', i: 'world'} \#{t 'ca.e', i: 'world'}
83
- p \#{t 'missing_in_es.a'} \#{t 'same_in_es.a'} \#{t 'blank_in_es.a'}
84
- p = t 'used_but_missing.a'
85
- p = t 'ignored_missing_key.a'
86
- p = t 'ignore.a'
87
- p = t 'ignored_pattern.some_key'
88
- p = t 'ignore_eq_base_all.a'
89
- p = t 'ignore_eq_base_es.a'
90
- SLIM
91
- 'app/views/relative/index.html.slim' => <<-SLIM,
92
- p = t '.title'
93
- SLIM
94
- 'app/controllers/events_controller.slim' => <<-RUBY,
95
- class EventsController < ApplicationController
96
- def show
97
- redirect_to :edit, notice: I18n.t('cb.a')
98
- I18n.t("cb.b", i: "Hello")
99
- I18n.t("hash_pattern.\#{some_value}", i: "Hello")
100
- I18n.t("hash_pattern2." + some_value, i: "Hello")
101
- end
102
- end
103
- RUBY
104
- # test that our algorithms can scale to the order of {BENCH_KEYS} keys.
105
- 'app/heavy.file' => BENCH_KEYS.times.map { |i| "t('bench.key#{i}') " }.join
68
+ # test that our algorithms can scale to the order of {BENCH_KEYS} keys.
69
+ 'vendor/heavy.file' => BENCH_KEYS.times.map { |i| "t('bench.key#{i}') " }.join
106
70
  }
107
71
  TestCodebase.setup fs
108
72
  end
data/spec/spec_helper.rb CHANGED
@@ -1,14 +1,18 @@
1
1
  ENV['RAKE_ENV'] ||= 'test'
2
2
  require 'rspec/autorun'
3
3
  $: << File.expand_path('../lib', __FILE__)
4
+
4
5
  require 'i18n/tasks'
5
6
  require 'rake'
7
+
6
8
  Rake.load_rakefile 'tasks/i18n-tasks.rake'
7
9
  Rake.load_rakefile 'support/test_codebase_env.rake'
10
+
8
11
  require 'term/ansicolor'
9
12
  Term::ANSIColor::coloring = false
13
+
10
14
  Dir['spec/support/**/*.rb'].each { |f| require "./#{f}" }
11
15
 
12
16
  RSpec.configure do |config|
13
- end
14
-
17
+ config.include FixturesSupport
18
+ end
@@ -0,0 +1,7 @@
1
+ # quick'n'dirty fixture loader
2
+ module FixturesSupport
3
+ def load_fixture(path)
4
+ Pathname.new('spec/fixtures').join(path).expand_path.read
5
+ end
6
+ end
7
+
@@ -1,25 +1,35 @@
1
1
  # coding: utf-8
2
2
  RSpec::Matchers.define :be_i18n_keys do |expected|
3
- locale_re = /^\w{2}\b/
4
- extract_keys = ->(actual){
3
+ def locale_re
4
+ /^\w{2}\b/
5
+ end
6
+
7
+ def extract_keys(actual)
5
8
  locales = I18n.available_locales.map(&:to_s)
6
9
  actual.split("\n").map { |x|
7
10
  x.strip!
8
11
  key = x.gsub(/\s+/, ' ').split(' ').reverse.detect { |p| p && p.include?('.') }
9
- if x =~ locale_re && locales.include?(x[0..1]) && !(key =~ locale_re && locales.include(key[0..1]))
12
+ if x =~ locale_re && locales.include?(x[0..1]) && !(key =~ locale_re && locales.include?(key[0..1]))
10
13
  x.split(' ', 2)[0] + '.' + key
11
14
  else
12
15
  key
13
16
  end
14
17
  }
15
- }
18
+ end
16
19
 
17
20
  match do |actual|
18
- extract_keys.(actual).sort == expected.sort
21
+ extract_keys(actual).should =~ expected
19
22
  end
20
23
 
21
24
  failure_message_for_should do |actual|
22
- "Expected #{expected.sort}, but had #{extract_keys.(actual).sort}"
25
+ e = expected.sort
26
+ a = extract_keys(actual).sort
27
+
28
+ <<-MSG.strip
29
+ Expected #{e}, but had #{a}. Diff:
23
30
 
31
+ missing: #{e-a}
32
+ extra: #{a-e}
33
+ MSG
24
34
  end
25
35
  end
@@ -1,11 +1,12 @@
1
1
  require 'fileutils'
2
+
2
3
  module TestCodebase
3
4
  extend self
4
5
  AT = 'tmp/test_codebase'
5
6
 
6
7
  DEFAULTS = {
7
- 'config/locales/en.yml' => {'en' => {}}.to_yaml,
8
- 'config/locales/es.yml' => {'es' => {}}.to_yaml
8
+ 'config/locales/en.yml' => {'en' => {}}.to_yaml,
9
+ 'config/locales/es.yml' => {'es' => {}}.to_yaml
9
10
  }
10
11
 
11
12
  def setup(files)
@@ -38,13 +39,19 @@ module TestCodebase
38
39
  Dir.chdir pwd
39
40
  end
40
41
 
41
- private
42
+ def capture_stderr
43
+ err, $stderr = $stderr, StringIO.new
44
+ yield
45
+ $stderr.string
46
+ ensure
47
+ $stderr = err
48
+ end
49
+
42
50
  def capture_stdout
43
- out = StringIO.new
44
- $stdout = out
51
+ out, $stdout = $stdout, StringIO.new
45
52
  yield
46
- out.string
53
+ $stdout.string
47
54
  ensure
48
- $stdout = STDOUT
55
+ $stdout = out
49
56
  end
50
57
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - glebm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-28 00:00:00.000000000 Z
11
+ date: 2013-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - '>='
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: Rails I18n tasks to find missing / unused translations and more
98
112
  email:
99
113
  - glex.spb@gmail.com
@@ -103,24 +117,32 @@ extra_rdoc_files: []
103
117
  files:
104
118
  - .gitignore
105
119
  - .travis.yml
120
+ - CHANGES.md
106
121
  - Gemfile
107
122
  - LICENSE.txt
108
123
  - README.md
109
124
  - Rakefile
110
125
  - doc/img/i18n-missing.png
126
+ - doc/img/i18n-tasks.png
111
127
  - i18n-tasks.gemspec
112
128
  - lib/i18n/tasks.rb
113
129
  - lib/i18n/tasks/base_task.rb
114
130
  - lib/i18n/tasks/missing.rb
131
+ - lib/i18n/tasks/output/terminal.rb
115
132
  - lib/i18n/tasks/prefill.rb
116
133
  - lib/i18n/tasks/railtie.rb
117
134
  - lib/i18n/tasks/task_helpers.rb
118
135
  - lib/i18n/tasks/unused.rb
119
136
  - lib/i18n/tasks/version.rb
120
137
  - lib/tasks/i18n-tasks.rake
138
+ - spec/fixtures/app/assets/javascripts/application.js
139
+ - spec/fixtures/app/controllers/events_controller.rb
140
+ - spec/fixtures/app/views/index.html.slim
141
+ - spec/fixtures/app/views/relative/index.html.slim
142
+ - spec/fixtures/config/i18n-tasks.yml
121
143
  - spec/i18n_tasks_spec.rb
122
144
  - spec/spec_helper.rb
123
- - spec/support/0_test_codebase_env.rake
145
+ - spec/support/fixtures.rb
124
146
  - spec/support/i18n_tasks_output_matcher.rb
125
147
  - spec/support/test_codebase.rb
126
148
  - spec/support/test_codebase_env.rake
@@ -149,9 +171,15 @@ signing_key:
149
171
  specification_version: 4
150
172
  summary: Rails I18n tasks to find missing / unused translations and more
151
173
  test_files:
174
+ - spec/fixtures/app/assets/javascripts/application.js
175
+ - spec/fixtures/app/controllers/events_controller.rb
176
+ - spec/fixtures/app/views/index.html.slim
177
+ - spec/fixtures/app/views/relative/index.html.slim
178
+ - spec/fixtures/config/i18n-tasks.yml
152
179
  - spec/i18n_tasks_spec.rb
153
180
  - spec/spec_helper.rb
154
- - spec/support/0_test_codebase_env.rake
181
+ - spec/support/fixtures.rb
155
182
  - spec/support/i18n_tasks_output_matcher.rb
156
183
  - spec/support/test_codebase.rb
157
184
  - spec/support/test_codebase_env.rake
185
+ has_rdoc:
@@ -1,4 +0,0 @@
1
- task :environment do
2
- I18n.default_locale = 'en'
3
- I18n.available_locales = %w(en es)
4
- end