i18n-tasks 0.8.7 → 0.9.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -5
- data/CHANGES.md +3 -3
- data/Gemfile +1 -1
- data/README.md +4 -4
- data/bin/i18n-tasks +0 -1
- data/config/locales/en.yml +103 -102
- data/config/locales/ru.yml +1 -1
- data/i18n-tasks.gemspec +1 -2
- data/lib/i18n/tasks.rb +0 -1
- data/lib/i18n/tasks/base_task.rb +0 -1
- data/lib/i18n/tasks/cli.rb +1 -1
- data/lib/i18n/tasks/command/commander.rb +0 -1
- data/lib/i18n/tasks/command/commands/missing.rb +3 -15
- data/lib/i18n/tasks/command/commands/usages.rb +5 -6
- data/lib/i18n/tasks/command/option_parsers/locale.rb +1 -8
- data/lib/i18n/tasks/command_error.rb +5 -1
- data/lib/i18n/tasks/commands.rb +0 -1
- data/lib/i18n/tasks/configuration.rb +1 -9
- data/lib/i18n/tasks/console_context.rb +2 -2
- data/lib/i18n/tasks/data.rb +0 -1
- data/lib/i18n/tasks/data/adapter/json_adapter.rb +0 -1
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +0 -1
- data/lib/i18n/tasks/data/file_formats.rb +0 -1
- data/lib/i18n/tasks/data/file_system.rb +0 -1
- data/lib/i18n/tasks/data/file_system_base.rb +0 -1
- data/lib/i18n/tasks/data/router/conservative_router.rb +0 -1
- data/lib/i18n/tasks/data/router/pattern_router.rb +0 -1
- data/lib/i18n/tasks/data/tree/node.rb +6 -7
- data/lib/i18n/tasks/data/tree/nodes.rb +0 -1
- data/lib/i18n/tasks/data/tree/siblings.rb +29 -10
- data/lib/i18n/tasks/data/tree/traversal.rb +0 -3
- data/lib/i18n/tasks/google_translation.rb +0 -1
- data/lib/i18n/tasks/ignore_keys.rb +0 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +0 -1
- data/lib/i18n/tasks/logging.rb +0 -1
- data/lib/i18n/tasks/missing_keys.rb +0 -1
- data/lib/i18n/tasks/plural_keys.rb +0 -1
- data/lib/i18n/tasks/reports/base.rb +1 -2
- data/lib/i18n/tasks/reports/spreadsheet.rb +0 -1
- data/lib/i18n/tasks/reports/terminal.rb +41 -17
- data/lib/i18n/tasks/scanners/files/caching_file_finder.rb +32 -0
- data/lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb +24 -0
- data/lib/i18n/tasks/scanners/files/caching_file_reader.rb +27 -0
- data/lib/i18n/tasks/scanners/files/file_finder.rb +61 -0
- data/lib/i18n/tasks/scanners/files/file_reader.rb +18 -0
- data/lib/i18n/tasks/scanners/key_occurrences.rb +35 -0
- data/lib/i18n/tasks/scanners/occurence.rb +50 -0
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +97 -38
- data/lib/i18n/tasks/scanners/pattern_with_scope_scanner.rb +2 -3
- data/lib/i18n/tasks/scanners/relative_keys.rb +3 -4
- data/lib/i18n/tasks/scanners/scanner.rb +15 -0
- data/lib/i18n/tasks/scanners/scanner_multiplexer.rb +43 -0
- data/lib/i18n/tasks/unused_keys.rb +4 -5
- data/lib/i18n/tasks/used_keys.rb +76 -23
- data/lib/i18n/tasks/version.rb +1 -2
- data/spec/conservative_router_spec.rb +0 -1
- data/spec/file_system_data_spec.rb +0 -1
- data/spec/fixtures/app/controllers/events_controller.rb +1 -2
- data/spec/google_translate_spec.rb +0 -1
- data/spec/i18n_tasks_spec.rb +4 -15
- data/spec/key_pattern_matching_spec.rb +0 -1
- data/spec/locale_tree/siblings_spec.rb +0 -1
- data/spec/pattern_scanner_spec.rb +34 -36
- data/spec/plural_keys_spec.rb +0 -1
- data/spec/readme_spec.rb +0 -1
- data/spec/relative_keys_spec.rb +15 -10
- data/spec/scanners/files/caching_file_finder_provider_spec.rb +18 -0
- data/spec/scanners/files/caching_file_finder_spec.rb +39 -0
- data/spec/scanners/files/caching_file_reader_spec.rb +18 -0
- data/spec/scanners/files/file_finder_spec.rb +52 -0
- data/spec/scanners/files/file_reader_spec.rb +15 -0
- data/spec/scanners/scanner_multiplexer_spec.rb +26 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/capture_std.rb +0 -1
- data/spec/support/fixtures.rb +0 -1
- data/spec/support/i18n_tasks_output_matcher.rb +0 -1
- data/spec/support/key_pattern_matcher.rb +0 -1
- data/spec/support/keys_and_occurrences.rb +27 -0
- data/spec/support/test_codebase.rb +0 -1
- data/spec/support/trees.rb +1 -7
- data/spec/used_keys_spec.rb +15 -16
- data/templates/config/i18n-tasks.yml +9 -2
- metadata +29 -9
- data/lib/i18n/tasks/scanners/base_scanner.rb +0 -149
- data/spec/commands/missing_commands_spec.rb +0 -23
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'i18n/tasks/scanners/files/file_reader'
|
2
|
+
module I18n::Tasks::Scanners::Files
|
3
|
+
# Reads the files in 'rb' mode and UTF-8 encoding.
|
4
|
+
# Wraps a {FileReader} and caches the results.
|
5
|
+
#
|
6
|
+
# @note This class is thread-safe. All methods are cached.
|
7
|
+
# @since 0.9.0
|
8
|
+
class CachingFileReader < FileReader
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
super
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@cache = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return the contents of the file at the given path.
|
17
|
+
# The file is read in the 'rb' mode and UTF-8 encoding.
|
18
|
+
#
|
19
|
+
# @param (see FileReader#read_file)
|
20
|
+
# @return (see FileReader#read_file)
|
21
|
+
# @note This method is cached, it will only access the filesystem on the first invocation.
|
22
|
+
def read_file(path)
|
23
|
+
absolute_path = File.expand_path(path)
|
24
|
+
@cache[absolute_path] || @mutex.synchronize { @cache[absolute_path] ||= super }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module I18n::Tasks::Scanners::Files
|
2
|
+
# Finds the files in the specified search paths with support for exclusion / inclusion patterns.
|
3
|
+
#
|
4
|
+
# @since 0.9.0
|
5
|
+
class FileFinder
|
6
|
+
include I18n::Tasks::Logging
|
7
|
+
|
8
|
+
# @param paths [Array<String>] {Find.find}-compatible paths to traverse,
|
9
|
+
# absolute or relative to the working directory.
|
10
|
+
# @param include [Array<String>, nil] {File.fnmatch}-compatible patterns files to include.
|
11
|
+
# Files not matching any of the inclusion patterns will be excluded.
|
12
|
+
# @param exclude [Arry<String>] {File.fnmatch}-compatible patterns of files to exclude.
|
13
|
+
# Files matching any of the exclusion patterns will be excluded even if they match an inclusion pattern.
|
14
|
+
def initialize(paths: ['.'], include: nil, exclude: [])
|
15
|
+
raise 'paths argument is required' if paths.nil?
|
16
|
+
@paths = paths
|
17
|
+
@include = include
|
18
|
+
@exclude = exclude || []
|
19
|
+
end
|
20
|
+
|
21
|
+
# Traverse the paths and yield the matching ones.
|
22
|
+
#
|
23
|
+
# @yield [path]
|
24
|
+
# @yieldparam path [String] the path of the found file.
|
25
|
+
# @return [Array<of block results>]
|
26
|
+
def traverse_files
|
27
|
+
find_files.map { |path| yield path }
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array<String>] found files
|
31
|
+
def find_files
|
32
|
+
results = []
|
33
|
+
paths = @paths.select { |p| File.exist?(p) }
|
34
|
+
if paths.empty?
|
35
|
+
log_warn "None of the search.paths exist #{@paths.inspect}"
|
36
|
+
else
|
37
|
+
Find.find(*paths) do |path|
|
38
|
+
is_dir = File.directory?(path)
|
39
|
+
hidden = File.basename(path).start_with?('.') && !%w(. ./).include?(path)
|
40
|
+
not_incl = @include && !path_fnmatch_any?(path, @include)
|
41
|
+
excl = path_fnmatch_any?(path, @exclude)
|
42
|
+
if is_dir || hidden || not_incl || excl
|
43
|
+
Find.prune if is_dir && (hidden || excl)
|
44
|
+
else
|
45
|
+
results << path
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
results
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# @param path [String]
|
55
|
+
# @param globs [Array<String>]
|
56
|
+
# @return [Boolean]
|
57
|
+
def path_fnmatch_any?(path, globs)
|
58
|
+
globs.any? { |glob| File.fnmatch(glob, path) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module I18n::Tasks::Scanners::Files
|
2
|
+
# Reads the files in 'rb' mode and UTF-8 encoding.
|
3
|
+
#
|
4
|
+
# @since 0.9.0
|
5
|
+
class FileReader
|
6
|
+
|
7
|
+
# Return the contents of the file at the given path.
|
8
|
+
# The file is read in the 'rb' mode and UTF-8 encoding.
|
9
|
+
#
|
10
|
+
# @param path [String] Path to the file, absolute or relative to the working directory.
|
11
|
+
# @return [String] file contents
|
12
|
+
def read_file(path)
|
13
|
+
result = nil
|
14
|
+
File.open(path, 'rb', encoding: 'UTF-8') { |f| result = f.read }
|
15
|
+
result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'i18n/tasks/scanners/occurence'
|
2
|
+
|
3
|
+
module I18n::Tasks::Scanners
|
4
|
+
# A scanned key and all its occurrences.
|
5
|
+
#
|
6
|
+
# @note This is a value type. Equality and hash code are determined from the attributes.
|
7
|
+
class KeyOccurrences
|
8
|
+
# @return [String] the key.
|
9
|
+
attr_reader :key
|
10
|
+
|
11
|
+
# @return [Array<Occurrence>] the key's occurrences.
|
12
|
+
attr_reader :occurrences
|
13
|
+
|
14
|
+
def initialize(key:, occurrences:)
|
15
|
+
@key = key
|
16
|
+
@occurrences = occurrences
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
other.key == @key && other.occurrences == @occurrences
|
21
|
+
end
|
22
|
+
|
23
|
+
def eql?(other)
|
24
|
+
self == other
|
25
|
+
end
|
26
|
+
|
27
|
+
def hash
|
28
|
+
[@key, @occurrences].hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"KeyOccurrences(#{key.inspect}, [#{occurrences.map(&:inspect).join(', ')}])"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module I18n::Tasks::Scanners
|
2
|
+
# The occurrence of some key in a file.
|
3
|
+
#
|
4
|
+
# @note This is a value type. Equality and hash code are determined from the attributes.
|
5
|
+
class Occurrence
|
6
|
+
# @return [String] source path relative to the current working directory.
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
# @return [Fixnum] count of characters in the file before the occurrence.
|
10
|
+
attr_reader :pos
|
11
|
+
|
12
|
+
# @return [Fixnum] line number of the occurrence, counting from 1.
|
13
|
+
attr_reader :line_num
|
14
|
+
|
15
|
+
# @return [Fixnum] position of the start of the occurrence in the line, counting from 1.
|
16
|
+
attr_reader :line_pos
|
17
|
+
|
18
|
+
# @return [String] the line of the occurrence, excluding the last LF or CRLF.
|
19
|
+
attr_reader :line
|
20
|
+
|
21
|
+
# @param path [String]
|
22
|
+
# @param pos [Fixnum]
|
23
|
+
# @param line_num [Fixnum]
|
24
|
+
# @param line_pos [Fixnum]
|
25
|
+
# @param line [String]
|
26
|
+
def initialize(path:, pos:, line_num:, line_pos:, line:)
|
27
|
+
@path = path
|
28
|
+
@pos = pos
|
29
|
+
@line_num = line_num
|
30
|
+
@line_pos = line_pos
|
31
|
+
@line = line
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"Occurrence(#{@path}:#{@line_num}:#{@line_pos}:(#{@pos})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
other.path == @path && other.pos == @pos && other.line_num == @line_num && other.line == @line
|
40
|
+
end
|
41
|
+
|
42
|
+
def eql?(other)
|
43
|
+
self == other
|
44
|
+
end
|
45
|
+
|
46
|
+
def hash
|
47
|
+
[@path, @pos, @line_num, @line_pos, @line].hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,62 +1,117 @@
|
|
1
|
-
|
2
|
-
require 'i18n/tasks/scanners/
|
1
|
+
require 'i18n/tasks/scanners/scanner'
|
2
|
+
require 'i18n/tasks/scanners/relative_keys'
|
3
3
|
|
4
4
|
module I18n::Tasks::Scanners
|
5
|
-
#
|
6
|
-
|
7
|
-
|
5
|
+
# Scan for I18n.t usages using a simple regular expression.
|
6
|
+
class PatternScanner < Scanner
|
7
|
+
include RelativeKeys
|
8
|
+
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
config: {},
|
13
|
+
file_finder_provider: Files::CachingFileFinderProvider.new,
|
14
|
+
file_reader: Files::CachingFileReader.new)
|
15
|
+
@config = config
|
16
|
+
@file_reader = file_reader
|
17
|
+
|
18
|
+
@file_finder = file_finder_provider.get(**config.slice(:paths, :include, :exclude))
|
19
|
+
@pattern = config[:pattern].present? ? Regexp.new(config[:pattern]) : default_pattern
|
20
|
+
@ignore_lines_res = (config[:ignore_lines] || []).inject({}) { |h, (ext, re)| h.update(ext => Regexp.new(re)) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return (see Scanner#keys)
|
24
|
+
def keys
|
25
|
+
(@file_finder.traverse_files { |path|
|
26
|
+
scan_file(path)
|
27
|
+
}.reduce(:+) || []).group_by(&:first).map { |key, keys_occurrences|
|
28
|
+
KeyOccurrences.new(key: key, occurrences: keys_occurrences.map(&:second))
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
8
34
|
# Extract i18n keys from file based on the pattern which must capture the key literal.
|
9
|
-
# @return [Array<
|
10
|
-
def scan_file(path
|
35
|
+
# @return [Array<[key, occurrence]>] each occurrence found in the file
|
36
|
+
def scan_file(path)
|
11
37
|
keys = []
|
12
|
-
|
13
|
-
text
|
14
|
-
|
15
|
-
src_pos = Regexp.last_match.offset(0).first
|
38
|
+
text = @file_reader.read_file(path)
|
39
|
+
text.scan(@pattern) do |match|
|
40
|
+
src_pos = Regexp.last_match.offset(0).first
|
16
41
|
location = src_location(path, text, src_pos)
|
17
|
-
next if exclude_line?(location
|
42
|
+
next if exclude_line?(location.line, path)
|
18
43
|
key = match_to_key(match, path, location)
|
19
44
|
next unless key
|
20
45
|
key = key + ':' if key.end_with?('.')
|
21
|
-
next unless valid_key?(key
|
22
|
-
keys << [key,
|
46
|
+
next unless valid_key?(key)
|
47
|
+
keys << [key, location]
|
23
48
|
end
|
24
49
|
keys
|
25
50
|
rescue Exception => e
|
26
|
-
raise ::I18n::Tasks::CommandError.new("Error scanning #{path}: #{e.message}")
|
27
|
-
end
|
28
|
-
|
29
|
-
def default_pattern
|
30
|
-
# capture only the first argument
|
31
|
-
/
|
32
|
-
#{translate_call_re} [\( ] \s* (?# fn call begin )
|
33
|
-
(#{literal_re}) (?# capture the first argument)
|
34
|
-
/x
|
51
|
+
raise ::I18n::Tasks::CommandError.new(e, "Error scanning #{path}: #{e.message}")
|
35
52
|
end
|
36
53
|
|
37
|
-
protected
|
38
|
-
|
39
|
-
# Given
|
40
54
|
# @param [MatchData] match
|
41
55
|
# @param [String] path
|
42
56
|
# @return [String] full absolute key name
|
43
57
|
def match_to_key(match, path, location)
|
44
|
-
|
45
|
-
|
58
|
+
absolute_key(strip_literal(match[0]), path, location)
|
59
|
+
end
|
60
|
+
|
61
|
+
def exclude_line?(line, path)
|
62
|
+
re = @ignore_lines_res[File.extname(path)[1..-1]]
|
63
|
+
re && re =~ line
|
46
64
|
end
|
47
65
|
|
48
66
|
def absolute_key(key, path, location)
|
49
67
|
if key.start_with?('.')
|
50
68
|
if controller_file?(path) || mailer_file?(path)
|
51
|
-
absolutize_key(key, path, relative_roots, closest_method(location))
|
69
|
+
absolutize_key(key, path, config[:relative_roots], closest_method(location))
|
52
70
|
else
|
53
|
-
absolutize_key(key, path)
|
71
|
+
absolutize_key(key, path, config[:relative_roots])
|
54
72
|
end
|
55
73
|
else
|
56
74
|
key
|
57
75
|
end
|
58
76
|
end
|
59
77
|
|
78
|
+
# @param path [String]
|
79
|
+
# @param text [String] contents of the file at the path.
|
80
|
+
# @param src_pos [Fixnum] position just before the beginning of the match.
|
81
|
+
# @return [Occurrence]
|
82
|
+
def src_location(path, text, src_pos)
|
83
|
+
line_begin = text.rindex(/^/, src_pos - 1)
|
84
|
+
line_end = text.index(/.(?=\r?\n|$)/, src_pos)
|
85
|
+
Occurrence.new(
|
86
|
+
path: path,
|
87
|
+
pos: src_pos,
|
88
|
+
line_num: text[0..src_pos].count("\n") + 1,
|
89
|
+
line_pos: src_pos - line_begin + 1,
|
90
|
+
line: text[line_begin..line_end])
|
91
|
+
end
|
92
|
+
|
93
|
+
# remove the leading colon and unwrap quotes from the key match
|
94
|
+
# @param literal [String] e.g: "key", 'key', or :key.
|
95
|
+
# @return [String] key
|
96
|
+
def strip_literal(literal)
|
97
|
+
key = literal
|
98
|
+
key = key[1..-1] if ':' == key[0]
|
99
|
+
key = key[1..-2] if %w(' ").include?(key[0])
|
100
|
+
key
|
101
|
+
end
|
102
|
+
|
103
|
+
VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!;À-ž])/
|
104
|
+
VALID_KEY_RE_STRICT = /^#{VALID_KEY_CHARS}+$/
|
105
|
+
VALID_KEY_RE = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
|
106
|
+
|
107
|
+
def valid_key?(key)
|
108
|
+
if @config[:strict]
|
109
|
+
key =~ VALID_KEY_RE_STRICT && !key.end_with?('.')
|
110
|
+
else
|
111
|
+
key =~ VALID_KEY_RE
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
60
115
|
def controller_file?(path)
|
61
116
|
/controllers/.match(path)
|
62
117
|
end
|
@@ -65,14 +120,10 @@ module I18n::Tasks::Scanners
|
|
65
120
|
/mailers/.match(path)
|
66
121
|
end
|
67
122
|
|
68
|
-
def closest_method(
|
69
|
-
method = File.readlines(
|
70
|
-
|
71
|
-
method
|
72
|
-
end
|
73
|
-
|
74
|
-
def pattern
|
75
|
-
@pattern ||= config[:pattern].present? ? Regexp.new(config[:pattern]) : default_pattern
|
123
|
+
def closest_method(occurrence)
|
124
|
+
method = File.readlines(occurrence.path, encoding: 'UTF-8').
|
125
|
+
first(occurrence.line_num - 1).reverse_each.find { |x| x =~ /\bdef\b/ }
|
126
|
+
method && method.strip.sub(/^def\s*/, '').sub(/[\(\s;].*$/, '')
|
76
127
|
end
|
77
128
|
|
78
129
|
def translate_call_re
|
@@ -85,5 +136,13 @@ module I18n::Tasks::Scanners
|
|
85
136
|
def literal_re
|
86
137
|
/:?".+?"|:?'.+?'|:\w+/
|
87
138
|
end
|
139
|
+
|
140
|
+
def default_pattern
|
141
|
+
# capture only the first argument
|
142
|
+
/
|
143
|
+
#{translate_call_re} [\( ] \s* (?# fn call begin )
|
144
|
+
(#{literal_re}) (?# capture the first argument)
|
145
|
+
/x
|
146
|
+
end
|
88
147
|
end
|
89
148
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
require 'i18n/tasks/scanners/pattern_scanner'
|
3
2
|
|
4
3
|
module I18n::Tasks::Scanners
|
@@ -7,6 +6,8 @@ module I18n::Tasks::Scanners
|
|
7
6
|
# Caveat: scope is only detected when it is the first argument
|
8
7
|
class PatternWithScopeScanner < PatternScanner
|
9
8
|
|
9
|
+
protected
|
10
|
+
|
10
11
|
def default_pattern
|
11
12
|
# capture the first argument and scope argument if present
|
12
13
|
/#{super}
|
@@ -14,8 +15,6 @@ module I18n::Tasks::Scanners
|
|
14
15
|
/x
|
15
16
|
end
|
16
17
|
|
17
|
-
protected
|
18
|
-
|
19
18
|
# Given
|
20
19
|
# @param [MatchData] match
|
21
20
|
# @param [String] path
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
module I18n
|
3
2
|
module Tasks
|
4
3
|
module Scanners
|
@@ -7,13 +6,13 @@ module I18n
|
|
7
6
|
# @param path [String] path to the file containing the key
|
8
7
|
# @return [String] absolute version of the key
|
9
8
|
def absolutize_key(key, path, roots = relative_roots, closest_method = "")
|
9
|
+
fail 'roots argument is required' if roots.nil?
|
10
10
|
normalized_path = File.expand_path(path)
|
11
11
|
path_root(normalized_path, roots) or
|
12
|
-
|
12
|
+
fail CommandError.new(
|
13
13
|
"Error scanning #{normalized_path}: cannot resolve relative key
|
14
14
|
\"#{key}\".\nSet search.relative_roots in config/i18n-tasks.yml
|
15
|
-
(currently #{
|
16
|
-
)
|
15
|
+
(currently #{roots.inspect})")
|
17
16
|
|
18
17
|
prefix_key_based_on_path(key, normalized_path, roots, closest_method: closest_method)
|
19
18
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'i18n/tasks/scanners/key_occurrences'
|
2
|
+
|
3
|
+
module I18n::Tasks::Scanners
|
4
|
+
# Describes the API of a scanner.
|
5
|
+
#
|
6
|
+
# @abstract
|
7
|
+
# @since 0.9.0
|
8
|
+
class Scanner
|
9
|
+
# @abstract
|
10
|
+
# @return [Array<KeyOccurrences>] the keys found by this scanner and their occurrences.
|
11
|
+
def keys
|
12
|
+
raise 'Unimplemented'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'i18n/tasks/scanners/scanner'
|
2
|
+
|
3
|
+
module I18n::Tasks::Scanners
|
4
|
+
# Run multiple {Scanner Scanners} and merge their results.
|
5
|
+
# @note The scanners are run concurrently. A thread is spawned per each scanner.
|
6
|
+
# @since 0.9.0
|
7
|
+
class ScannerMultiplexer < Scanner
|
8
|
+
# @param scanners [Array<Scanner>]
|
9
|
+
def initialize(scanners:)
|
10
|
+
@scanners = scanners
|
11
|
+
end
|
12
|
+
|
13
|
+
# Collect the results of all the scanners. Occurrences of a key from multiple scanners are merged.
|
14
|
+
#
|
15
|
+
# @note The scanners are run concurrently. A thread is spawned per each scanner.
|
16
|
+
# @return (see Scanner#keys)
|
17
|
+
def keys
|
18
|
+
collect_results.inject({}) { |results_by_key, key_occurences|
|
19
|
+
key_occurences.each do |key_occurrence|
|
20
|
+
(results_by_key[key_occurrence.key] ||= []) << key_occurrence.occurrences
|
21
|
+
end
|
22
|
+
results_by_key
|
23
|
+
}.map { |key, all_occurrences|
|
24
|
+
occurrences = all_occurrences.flatten(1)
|
25
|
+
occurrences.sort_by!(&:path)
|
26
|
+
occurrences.uniq!
|
27
|
+
KeyOccurrences.new(key: key, occurrences: occurrences)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @return Array<Array<KeyOccurrences>>
|
34
|
+
def collect_results
|
35
|
+
return [@scanners[0].keys] if @scanners.length == 1
|
36
|
+
Array.new(@scanners.length).tap do |results|
|
37
|
+
@scanners.map.with_index { |scanner, i|
|
38
|
+
Thread.start { results[i] = scanner.keys }
|
39
|
+
}.each(&:join)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|