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