lib/i18n/tasks/scanners/pattern_scanner.rb in i18n-tasks-0.8.7 vs lib/i18n/tasks/scanners/pattern_scanner.rb in i18n-tasks-0.9.0.rc1
- old
+ new
@@ -1,89 +1,148 @@
-# coding: utf-8
-require 'i18n/tasks/scanners/base_scanner'
+require 'i18n/tasks/scanners/scanner'
+require 'i18n/tasks/scanners/relative_keys'
module I18n::Tasks::Scanners
- # Scans for I18n.t usages
- #
- class PatternScanner < BaseScanner
+ # Scan for I18n.t usages using a simple regular expression.
+ class PatternScanner < Scanner
+ include RelativeKeys
+
+ attr_reader :config
+
+ def initialize(
+ config: {},
+ file_finder_provider: Files::CachingFileFinderProvider.new,
+ file_reader: Files::CachingFileReader.new)
+ @config = config
+ @file_reader = file_reader
+
+ @file_finder = file_finder_provider.get(**config.slice(:paths, :include, :exclude))
+ @pattern = config[:pattern].present? ? Regexp.new(config[:pattern]) : default_pattern
+ @ignore_lines_res = (config[:ignore_lines] || []).inject({}) { |h, (ext, re)| h.update(ext => Regexp.new(re)) }
+ end
+
+ # @return (see Scanner#keys)
+ def keys
+ (@file_finder.traverse_files { |path|
+ scan_file(path)
+ }.reduce(:+) || []).group_by(&:first).map { |key, keys_occurrences|
+ KeyOccurrences.new(key: key, occurrences: keys_occurrences.map(&:second))
+ }
+ end
+
+ protected
+
# Extract i18n keys from file based on the pattern which must capture the key literal.
- # @return [Array<Key>] keys found in file
- def scan_file(path, opts = {})
+ # @return [Array<[key, occurrence]>] each occurrence found in the file
+ def scan_file(path)
keys = []
- strict = !!opts[:strict]
- text = opts[:text] || read_file(path)
- text.scan(pattern) do |match|
- src_pos = Regexp.last_match.offset(0).first
+ text = @file_reader.read_file(path)
+ text.scan(@pattern) do |match|
+ src_pos = Regexp.last_match.offset(0).first
location = src_location(path, text, src_pos)
- next if exclude_line?(location[:line], path)
+ next if exclude_line?(location.line, path)
key = match_to_key(match, path, location)
next unless key
key = key + ':' if key.end_with?('.')
- next unless valid_key?(key, strict)
- keys << [key, data: location]
+ next unless valid_key?(key)
+ keys << [key, location]
end
keys
rescue Exception => e
- raise ::I18n::Tasks::CommandError.new("Error scanning #{path}: #{e.message}")
+ raise ::I18n::Tasks::CommandError.new(e, "Error scanning #{path}: #{e.message}")
end
- def default_pattern
- # capture only the first argument
- /
- #{translate_call_re} [\( ] \s* (?# fn call begin )
- (#{literal_re}) (?# capture the first argument)
- /x
- end
-
- protected
-
- # Given
# @param [MatchData] match
# @param [String] path
# @return [String] full absolute key name
def match_to_key(match, path, location)
- key = strip_literal(match[0])
- absolute_key(key, path, location)
+ absolute_key(strip_literal(match[0]), path, location)
end
+ def exclude_line?(line, path)
+ re = @ignore_lines_res[File.extname(path)[1..-1]]
+ re && re =~ line
+ end
+
def absolute_key(key, path, location)
if key.start_with?('.')
if controller_file?(path) || mailer_file?(path)
- absolutize_key(key, path, relative_roots, closest_method(location))
+ absolutize_key(key, path, config[:relative_roots], closest_method(location))
else
- absolutize_key(key, path)
+ absolutize_key(key, path, config[:relative_roots])
end
else
key
end
end
+ # @param path [String]
+ # @param text [String] contents of the file at the path.
+ # @param src_pos [Fixnum] position just before the beginning of the match.
+ # @return [Occurrence]
+ def src_location(path, text, src_pos)
+ line_begin = text.rindex(/^/, src_pos - 1)
+ line_end = text.index(/.(?=\r?\n|$)/, src_pos)
+ Occurrence.new(
+ path: path,
+ pos: src_pos,
+ line_num: text[0..src_pos].count("\n") + 1,
+ line_pos: src_pos - line_begin + 1,
+ line: text[line_begin..line_end])
+ end
+
+ # remove the leading colon and unwrap quotes from the key match
+ # @param literal [String] e.g: "key", 'key', or :key.
+ # @return [String] key
+ def strip_literal(literal)
+ key = literal
+ key = key[1..-1] if ':' == key[0]
+ key = key[1..-2] if %w(' ").include?(key[0])
+ key
+ end
+
+ VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!;À-ž])/
+ VALID_KEY_RE_STRICT = /^#{VALID_KEY_CHARS}+$/
+ VALID_KEY_RE = /^(#{VALID_KEY_CHARS}|[:\#{@}\[\]])+$/
+
+ def valid_key?(key)
+ if @config[:strict]
+ key =~ VALID_KEY_RE_STRICT && !key.end_with?('.')
+ else
+ key =~ VALID_KEY_RE
+ end
+ end
+
def controller_file?(path)
/controllers/.match(path)
end
def mailer_file?(path)
/mailers/.match(path)
end
- def closest_method(location)
- method = File.readlines(location[:src_path], encoding: 'UTF-8').first(location[:line_num] - 1).reverse_each.find { |x| x=~ /\bdef\b/ }
- method &&= method.strip.sub(/^def\s*/, '').sub(/[\(\s;].*$/, '')
- method
+ def closest_method(occurrence)
+ method = File.readlines(occurrence.path, encoding: 'UTF-8').
+ first(occurrence.line_num - 1).reverse_each.find { |x| x =~ /\bdef\b/ }
+ method && method.strip.sub(/^def\s*/, '').sub(/[\(\s;].*$/, '')
end
- def pattern
- @pattern ||= config[:pattern].present? ? Regexp.new(config[:pattern]) : default_pattern
- end
-
def translate_call_re
/(?<=^|[^\w'\-])t(?:ranslate)?/
end
# Match literals:
# * String: '', "#{}"
# * Symbol: :sym, :'', :"#{}"
def literal_re
/:?".+?"|:?'.+?'|:\w+/
+ end
+
+ def default_pattern
+ # capture only the first argument
+ /
+ #{translate_call_re} [\( ] \s* (?# fn call begin )
+ (#{literal_re}) (?# capture the first argument)
+ /x
end
end
end