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 +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 [](https://travis-ci.org/glebm/i18n-tasks) [](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
|
+

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