i18n-tasks 0.1.1 → 0.1.2

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.
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