i18n-tasks 0.2.22 → 0.3.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|