i18n-tasks 0.8.7 → 0.9.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/.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
|