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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +2 -0
- data/CHANGES.md +4 -0
- data/README.md +28 -13
- data/doc/img/i18n-tasks.png +0 -0
- data/i18n-tasks.gemspec +1 -0
- data/lib/i18n/tasks/base_task.rb +68 -21
- data/lib/i18n/tasks/missing.rb +35 -57
- data/lib/i18n/tasks/output/terminal.rb +44 -0
- data/lib/i18n/tasks/prefill.rb +1 -1
- data/lib/i18n/tasks/task_helpers.rb +27 -10
- data/lib/i18n/tasks/unused.rb +4 -17
- data/lib/i18n/tasks/version.rb +1 -1
- data/lib/tasks/i18n-tasks.rake +5 -4
- data/spec/fixtures/app/assets/javascripts/application.js +3 -0
- data/spec/fixtures/app/controllers/events_controller.rb +9 -0
- data/spec/fixtures/app/views/index.html.slim +9 -0
- data/spec/fixtures/app/views/relative/index.html.slim +1 -0
- data/spec/fixtures/config/i18n-tasks.yml +33 -0
- data/spec/i18n_tasks_spec.rb +32 -68
- data/spec/spec_helper.rb +6 -2
- data/spec/support/fixtures.rb +7 -0
- data/spec/support/i18n_tasks_output_matcher.rb +16 -6
- data/spec/support/test_codebase.rb +14 -7
- metadata +32 -4
- data/spec/support/0_test_codebase_env.rake +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c0488a6ce907e9327da7ed07ffea723e8f8010f
|
4
|
+
data.tar.gz: 8f5042c6c785f81d5c75bc953fcd7d2cce496870
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9297eb48c4d50a91e4825b2a3867360f0296f780bfecb7fd9e6ae39b98501ca916557f3ab12501be96ab45eccfbad46628d315b297edf761887d6c9471a98579
|
7
|
+
data.tar.gz: 6eebe2d12de95e7e8563409007dd2df906768e7860047370d3e5cd9952b6382ac8abcb3096b14e404431c9477c5a54793e4a2068df956346d57536c4338008f2
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGES.md
ADDED
data/README.md
CHANGED
@@ -1,17 +1,17 @@
|
|
1
|
-
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](
|
11
|
-
* `i18n:prefill` task normalizes locale files, and adds missing keys from base locale to others *([source](
|
12
|
-
* `i18n:unused` task shows potentially unused translations *([source](
|
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](
|
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
data/lib/i18n/tasks/base_task.rb
CHANGED
@@ -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
|
25
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
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,
|
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,
|
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
|
-
|
78
|
-
|
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
|
data/lib/i18n/tasks/missing.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
71
|
-
|
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
|
data/lib/i18n/tasks/prefill.rb
CHANGED
@@ -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 =>
|
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
|
-
|
9
|
-
|
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
|
-
#
|
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:
|
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
|
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
|
-
|
39
|
+
patterns = global + type_ignore
|
34
40
|
elsif type_ignore.is_a?(Hash)
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
data/lib/i18n/tasks/unused.rb
CHANGED
@@ -4,24 +4,11 @@ require 'i18n/tasks/base_task'
|
|
4
4
|
module I18n
|
5
5
|
module Tasks
|
6
6
|
class Unused < BaseTask
|
7
|
-
|
8
|
-
def
|
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
|
-
|
20
|
-
|
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
|
data/lib/i18n/tasks/version.rb
CHANGED
data/lib/tasks/i18n-tasks.rake
CHANGED
@@ -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
|
18
|
-
|
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.
|
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.
|
26
|
+
I18n::Tasks::Output::Terminal.new.unused I18n::Tasks::Unused.new.find_keys
|
26
27
|
end
|
27
28
|
end
|
@@ -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'
|
data/spec/i18n_tasks_spec.rb
CHANGED
@@ -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.
|
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.
|
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 =
|
30
|
+
BENCH_KEYS = 30
|
27
31
|
before do
|
28
32
|
gen_data = ->(v) {
|
29
33
|
{
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
48
|
+
BENCH_KEYS.times { |i| gen["key#{i}"] = v }
|
46
49
|
}
|
47
50
|
}
|
48
51
|
|
49
|
-
en_data
|
50
|
-
es_data
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
#
|
77
|
-
|
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
|
-
|
14
|
-
|
17
|
+
config.include FixturesSupport
|
18
|
+
end
|
@@ -1,25 +1,35 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
RSpec::Matchers.define :be_i18n_keys do |expected|
|
3
|
-
locale_re
|
4
|
-
|
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
|
21
|
+
extract_keys(actual).should =~ expected
|
19
22
|
end
|
20
23
|
|
21
24
|
failure_message_for_should do |actual|
|
22
|
-
|
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
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
53
|
+
$stdout.string
|
47
54
|
ensure
|
48
|
-
$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.
|
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-
|
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/
|
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/
|
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:
|