i18n-tasks 0.9.37 → 1.0.13

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -5
  3. data/Rakefile +1 -1
  4. data/config/locales/en.yml +12 -3
  5. data/config/locales/ru.yml +8 -0
  6. data/i18n-tasks.gemspec +14 -7
  7. data/lib/i18n/tasks/cli.rb +8 -7
  8. data/lib/i18n/tasks/command/commander.rb +1 -0
  9. data/lib/i18n/tasks/command/commands/missing.rb +17 -5
  10. data/lib/i18n/tasks/command/options/common.rb +0 -1
  11. data/lib/i18n/tasks/command/options/data.rb +1 -1
  12. data/lib/i18n/tasks/concurrent/cached_value.rb +0 -2
  13. data/lib/i18n/tasks/configuration.rb +13 -7
  14. data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +15 -2
  15. data/lib/i18n/tasks/data/file_formats.rb +1 -1
  16. data/lib/i18n/tasks/data/router/pattern_router.rb +1 -1
  17. data/lib/i18n/tasks/data/tree/node.rb +1 -1
  18. data/lib/i18n/tasks/data/tree/nodes.rb +5 -7
  19. data/lib/i18n/tasks/data/tree/siblings.rb +1 -2
  20. data/lib/i18n/tasks/data/tree/traversal.rb +25 -11
  21. data/lib/i18n/tasks/html_keys.rb +2 -2
  22. data/lib/i18n/tasks/interpolations.rb +7 -3
  23. data/lib/i18n/tasks/key_pattern_matching.rb +2 -0
  24. data/lib/i18n/tasks/locale_pathname.rb +1 -1
  25. data/lib/i18n/tasks/plural_keys.rb +0 -6
  26. data/lib/i18n/tasks/references.rb +3 -3
  27. data/lib/i18n/tasks/reports/base.rb +2 -2
  28. data/lib/i18n/tasks/reports/terminal.rb +3 -3
  29. data/lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb +118 -0
  30. data/lib/i18n/tasks/scanners/ast_matchers/message_receivers_matcher.rb +91 -0
  31. data/lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb +69 -0
  32. data/lib/i18n/tasks/scanners/erb_ast_processor.rb +74 -0
  33. data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
  34. data/lib/i18n/tasks/scanners/local_ruby_parser.rb +85 -0
  35. data/lib/i18n/tasks/scanners/pattern_mapper.rb +1 -1
  36. data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -2
  37. data/lib/i18n/tasks/scanners/relative_keys.rb +2 -2
  38. data/lib/i18n/tasks/scanners/results/occurrence.rb +17 -1
  39. data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +9 -34
  40. data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +91 -156
  41. data/lib/i18n/tasks/scanners/ruby_key_literals.rb +2 -2
  42. data/lib/i18n/tasks/split_key.rb +1 -1
  43. data/lib/i18n/tasks/translation.rb +4 -1
  44. data/lib/i18n/tasks/translators/base_translator.rb +17 -12
  45. data/lib/i18n/tasks/translators/deepl_translator.rb +34 -11
  46. data/lib/i18n/tasks/translators/google_translator.rb +1 -1
  47. data/lib/i18n/tasks/translators/openai_translator.rb +100 -0
  48. data/lib/i18n/tasks/used_keys.rb +9 -6
  49. data/lib/i18n/tasks/version.rb +1 -1
  50. data/lib/i18n/tasks.rb +11 -0
  51. data/templates/config/i18n-tasks.yml +17 -4
  52. data/templates/minitest/i18n_test.rb +6 -6
  53. metadata +74 -16
@@ -52,15 +52,9 @@ module I18n::Tasks::PluralKeys
52
52
  end
53
53
 
54
54
  def plural_forms?(s)
55
- return false if non_plural_other?(s)
56
-
57
55
  s.present? && s.all? { |node| node.leaf? && plural_suffix?(node.key) }
58
56
  end
59
57
 
60
- def non_plural_other?(s)
61
- s.size == 1 && s.first.leaf? && (!s.first.value.is_a?(String) || !s.first.value.include?('%{count}'))
62
- end
63
-
64
58
  def plural_suffix?(key)
65
59
  PLURAL_KEY_SUFFIXES.include?(key)
66
60
  end
@@ -90,9 +90,9 @@ module I18n::Tasks
90
90
  on_leaves_merge: lambda do |node, other|
91
91
  if node.value != other.value
92
92
  log_warn(
93
- 'Conflicting references: '\
94
- "#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]},"\
95
- " but ⮕ #{other.value} in #{other.data[:locale]}"
93
+ 'Conflicting references: ' \
94
+ "#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]}, " \
95
+ "but ⮕ #{other.value} in #{other.data[:locale]}"
96
96
  )
97
97
  end
98
98
  end
@@ -36,8 +36,8 @@ module I18n::Tasks::Reports
36
36
 
37
37
  def used_title(keys_nodes, filter)
38
38
  used_n = keys_nodes.map { |_k, node| node.data[:occurrences].size }.reduce(:+).to_i
39
- "#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}"\
40
- "#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
39
+ "#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}" \
40
+ "#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n.positive?}"
41
41
  end
42
42
 
43
43
  # Sort keys by their attributes in order
@@ -112,7 +112,7 @@ module I18n
112
112
  when :missing_plural
113
113
  leaf[:data][:missing_keys].join(', ')
114
114
  else
115
- "#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} "\
115
+ "#{Rainbow(leaf[:data][:missing_diff_locale]).cyan} " \
116
116
  "#{format_value(leaf[:value].is_a?(String) ? leaf[:value].strip : leaf[:value])}"
117
117
  end
118
118
  end
@@ -121,9 +121,9 @@ module I18n
121
121
  if data[:ref_info]
122
122
  from, to = data[:ref_info]
123
123
  resolved = key[0...to.length]
124
- after = key[to.length..-1]
124
+ after = key[to.length..]
125
125
  " #{Rainbow(from).yellow}#{Rainbow(after).cyan}\n" \
126
- "#{Rainbow('⮕').yellow.bright} #{Rainbow(resolved).yellow.bright}"
126
+ "#{Rainbow('⮕').yellow.bright} #{Rainbow(resolved).yellow.bright}"
127
127
  else
128
128
  Rainbow(key).cyan
129
129
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n::Tasks::Scanners::AstMatchers
4
+ class BaseMatcher
5
+ def initialize(scanner:)
6
+ @scanner = scanner
7
+ end
8
+
9
+ def convert_to_key_occurrences(send_node, _method_name, location: send_node.loc)
10
+ fail('Not implemented')
11
+ end
12
+
13
+ protected
14
+
15
+ # If the node type is of `%i(sym str int false true)`, return the value as a string.
16
+ # Otherwise, if `config[:strict]` is `false` and the type is of `%i(dstr dsym)`,
17
+ # return the source as if it were a string.
18
+ #
19
+ # @param node [Parser::AST::Node]
20
+ # @param array_join_with [String, nil] if set to a string, arrays will be processed and their elements joined.
21
+ # @param array_flatten [Boolean] if true, nested arrays are flattened,
22
+ # otherwise their source is copied and surrounded by #{}. No effect unless `array_join_with` is set.
23
+ # @param array_reject_blank [Boolean] if true, empty strings and `nil`s are skipped.
24
+ # No effect unless `array_join_with` is set.
25
+ # @return [String, nil] `nil` is returned only when a dynamic value is encountered in strict mode
26
+ # or the node type is not supported.
27
+ def extract_string(node, array_join_with: nil, array_flatten: false, array_reject_blank: false) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
28
+ return if node.nil?
29
+
30
+ if %i[sym str int].include?(node.type)
31
+ node.children[0].to_s
32
+ elsif %i[true false].include?(node.type)
33
+ node.type.to_s
34
+ elsif node.type == :nil
35
+ ''
36
+ elsif node.type == :array && array_join_with
37
+ extract_array_as_string(
38
+ node,
39
+ array_join_with: array_join_with,
40
+ array_flatten: array_flatten,
41
+ array_reject_blank: array_reject_blank
42
+ ).tap do |str|
43
+ # `nil` is returned when a dynamic value is encountered in strict mode. Propagate:
44
+ return nil if str.nil?
45
+ end
46
+ elsif !@scanner.config[:strict] && %i[dsym dstr].include?(node.type)
47
+ node.children.map do |child|
48
+ if %i[sym str].include?(child.type)
49
+ child.children[0].to_s
50
+ else
51
+ child.loc.expression.source
52
+ end
53
+ end.join
54
+ end
55
+ end
56
+
57
+ # Extract the whole hash from a node of type `:hash`
58
+ #
59
+ # @param node [AST::Node] a node of type `:hash`.
60
+ # @return [Hash] the whole hash from the node
61
+ def extract_hash(node)
62
+ return {} if node.nil?
63
+
64
+ if node.type == :hash
65
+ node.children.each_with_object({}) do |pair, h|
66
+ key = pair.children[0].children[0].to_s
67
+ value = pair.children[1].children[0]
68
+ h[key] = value
69
+ end
70
+ end
71
+ end
72
+
73
+ # Extract a hash pair with a given literal key.
74
+ #
75
+ # @param node [AST::Node] a node of type `:hash`.
76
+ # @param key [String] node key as a string (indifferent symbol-string matching).
77
+ # @return [AST::Node, nil] a node of type `:pair` or nil.
78
+ def extract_hash_pair(node, key)
79
+ node.children.detect do |child|
80
+ next unless child.type == :pair
81
+
82
+ key_node = child.children[0]
83
+ %i[sym str].include?(key_node.type) && key_node.children[0].to_s == key
84
+ end
85
+ end
86
+
87
+ # Extract an array as a single string.
88
+ #
89
+ # @param array_join_with [String] joiner of the array elements.
90
+ # @param array_flatten [Boolean] if true, nested arrays are flattened,
91
+ # otherwise their source is copied and surrounded by #{}.
92
+ # @param array_reject_blank [Boolean] if true, empty strings and `nil`s are skipped.
93
+ # @return [String, nil] `nil` is returned only when a dynamic value is encountered in strict mode.
94
+ def extract_array_as_string(node, array_join_with:, array_flatten: false, array_reject_blank: false)
95
+ children_strings = node.children.map do |child|
96
+ if %i[sym str int true false].include?(child.type)
97
+ extract_string child
98
+ else
99
+ # ignore dynamic argument in strict mode
100
+ return nil if @scanner.config[:strict]
101
+
102
+ if %i[dsym dstr].include?(child.type) || (child.type == :array && array_flatten)
103
+ extract_string(child, array_join_with: array_join_with)
104
+ else
105
+ "\#{#{child.loc.expression.source}}"
106
+ end
107
+ end
108
+ end
109
+ if array_reject_blank
110
+ children_strings.reject! do |x|
111
+ # empty strings and nils in the scope argument are ignored by i18n
112
+ x == ''
113
+ end
114
+ end
115
+ children_strings.join(array_join_with)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/scanners/ast_matchers/base_matcher'
4
+ require 'i18n/tasks/scanners/results/occurrence'
5
+
6
+ module I18n::Tasks::Scanners::AstMatchers
7
+ class MessageReceiversMatcher < BaseMatcher
8
+ def initialize(scanner:, receivers:, message:)
9
+ super(scanner: scanner)
10
+ @receivers = Array(receivers)
11
+ @message = message
12
+ end
13
+
14
+ # @param send_node [Parser::AST::Node]
15
+ # @param method_name [Symbol, nil]
16
+ # @param location [Parser::Source::Map]
17
+ # @return [nil, [key, Occurrence]] full absolute key name and the occurrence.
18
+ def convert_to_key_occurrences(send_node, method_name, location: send_node.loc)
19
+ return unless node_match?(send_node)
20
+
21
+ receiver = send_node.children[0]
22
+ first_arg_node = send_node.children[2]
23
+ second_arg_node = send_node.children[3]
24
+
25
+ key = extract_string(first_arg_node)
26
+ return if key.nil?
27
+
28
+ key, default_arg = process_options(node: second_arg_node, key: key)
29
+
30
+ return if key.nil?
31
+
32
+ [
33
+ full_key(receiver: receiver, key: key, location: location, calling_method: method_name),
34
+ I18n::Tasks::Scanners::Results::Occurrence.from_range(
35
+ raw_key: key,
36
+ range: location.expression,
37
+ default_arg: default_arg
38
+ )
39
+ ]
40
+ end
41
+
42
+ private
43
+
44
+ def node_match?(node)
45
+ receiver = node.children[0]
46
+ message = node.children[1]
47
+
48
+ @message == message && @receivers.any? { |r| r == receiver }
49
+ end
50
+
51
+ def full_key(receiver:, key:, location:, calling_method:)
52
+ if receiver.nil?
53
+ # Relative keys only work if called via `t()` but not `I18n.t()`:
54
+ @scanner.absolute_key(
55
+ key,
56
+ location.expression.source_buffer.name,
57
+ calling_method: calling_method
58
+ )
59
+ else
60
+ key
61
+ end
62
+ end
63
+
64
+ def process_options(node:, key:)
65
+ return [key, nil] if node&.type != :hash
66
+
67
+ scope_node = extract_hash_pair(node, 'scope')
68
+
69
+ if scope_node
70
+ scope = extract_string(
71
+ scope_node.children[1],
72
+ array_join_with: '.',
73
+ array_flatten: true,
74
+ array_reject_blank: true
75
+ )
76
+ return nil if scope.nil? && scope_node.type != :nil
77
+
78
+ key = [scope, key].join('.') unless scope == ''
79
+ end
80
+ if default_arg_node = extract_hash_pair(node, 'default')
81
+ default_arg = if default_arg_node.children[1]&.type == :hash
82
+ extract_hash(default_arg_node.children[1])
83
+ else
84
+ extract_string(default_arg_node.children[1])
85
+ end
86
+ end
87
+
88
+ [key, default_arg]
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/scanners/results/occurrence'
4
+
5
+ module I18n::Tasks::Scanners::AstMatchers
6
+ class RailsModelMatcher < BaseMatcher
7
+ def convert_to_key_occurrences(send_node, _method_name, location: send_node.loc)
8
+ human_attribute_name_to_key_occurences(send_node: send_node, location: location) ||
9
+ model_name_human_to_key_occurences(send_node: send_node, location: location)
10
+ end
11
+
12
+ private
13
+
14
+ def human_attribute_name_to_key_occurences(send_node:, location:)
15
+ children = Array(send_node&.children)
16
+ receiver = children[0]
17
+ method_name = children[1]
18
+
19
+ return unless method_name == :human_attribute_name && receiver.type == :const
20
+
21
+ value = children[2]
22
+
23
+ model_name = underscore(receiver.to_a.last)
24
+ attribute = extract_string(value)
25
+ key = "activerecord.attributes.#{model_name}.#{attribute}"
26
+ [
27
+ key,
28
+ I18n::Tasks::Scanners::Results::Occurrence.from_range(
29
+ raw_key: key,
30
+ range: location.expression
31
+ )
32
+ ]
33
+ end
34
+
35
+ # User.model_name.human(count: 2)
36
+ # s(:send,
37
+ # s(:send,
38
+ # s(:const, nil, :User), :model_name), :human,
39
+ # s(:hash,
40
+ # s(:pair,
41
+ # s(:sym, :count),
42
+ # s(:int, 2))))
43
+ def model_name_human_to_key_occurences(send_node:, location:)
44
+ children = Array(send_node&.children)
45
+ return unless children[1] == :human
46
+
47
+ base_children = Array(children[0]&.children)
48
+ class_node = base_children[0]
49
+
50
+ return unless class_node&.type == :const && base_children[1] == :model_name
51
+
52
+ model_name = underscore(class_node.to_a.last)
53
+ key = "activerecord.models.#{model_name}"
54
+ [
55
+ key,
56
+ I18n::Tasks::Scanners::Results::Occurrence.from_range(
57
+ raw_key: key,
58
+ range: location.expression
59
+ )
60
+ ]
61
+ end
62
+
63
+ def underscore(value)
64
+ value = value.dup.to_s
65
+ value.gsub!(/(.)([A-Z])/, '\1_\2')
66
+ value.downcase!
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ast'
4
+ require 'set'
5
+ require 'i18n/tasks/scanners/local_ruby_parser'
6
+
7
+ module I18n::Tasks::Scanners
8
+ class ErbAstProcessor
9
+ include AST::Processor::Mixin
10
+ def initialize
11
+ super()
12
+ @ruby_parser = LocalRubyParser.new(ignore_blocks: true)
13
+ @comments = []
14
+ end
15
+
16
+ def process_and_extract_comments(ast)
17
+ result = process(ast)
18
+ [result, @comments]
19
+ end
20
+
21
+ def on_code(node)
22
+ parsed, comments = @ruby_parser.parse(
23
+ node.children[0],
24
+ location: node.location
25
+ )
26
+ @comments.concat(comments)
27
+
28
+ unless parsed.nil?
29
+ parsed = parsed.updated(
30
+ nil,
31
+ parsed.children.map { |child| node?(child) ? process(child) : child }
32
+ )
33
+ node = node.updated(:send, parsed)
34
+ end
35
+ node
36
+ end
37
+
38
+ # @param node [::Parser::AST::Node]
39
+ # @return [::Parser::AST::Node]
40
+ def handler_missing(node)
41
+ node = handle_comment(node)
42
+ return if node.nil?
43
+
44
+ node.updated(
45
+ nil,
46
+ node.children.map { |child| node?(child) ? process(child) : child }
47
+ )
48
+ end
49
+
50
+ private
51
+
52
+ # Convert ERB-comments to ::Parser::Source::Comment and skip processing node
53
+ #
54
+ # @param node Parser::AST::Node Potential comment node
55
+ # @return Parser::AST::Node or nil
56
+ def handle_comment(node)
57
+ if node.type == :erb && node.children.size == 4 &&
58
+ node.children[0]&.type == :indicator && node.children[0].children[0] == '#' &&
59
+ node.children[2]&.type == :code
60
+
61
+ # Do not continue parsing this node
62
+ comment = node.children[2]
63
+ @comments << ::Parser::Source::Comment.new(comment.location.expression)
64
+ return
65
+ end
66
+
67
+ node
68
+ end
69
+
70
+ def node?(node)
71
+ node.is_a?(::Parser::AST::Node)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/tasks/scanners/ruby_ast_scanner'
4
+ require 'i18n/tasks/scanners/erb_ast_processor'
5
+ require 'better_html/errors'
6
+ require 'better_html/parser'
7
+
8
+ module I18n::Tasks::Scanners
9
+ # Scan for I18n.translate calls in ERB-file better-html and ASTs
10
+ class ErbAstScanner < RubyAstScanner
11
+ def initialize(**args)
12
+ super(**args)
13
+ @erb_ast_processor = ErbAstProcessor.new
14
+ end
15
+
16
+ private
17
+
18
+ # Parse file on path and returns AST and comments.
19
+ #
20
+ # @param path Path to file to parse
21
+ # @return [{Parser::AST::Node}, [Parser::Source::Comment]]
22
+ def path_to_ast_and_comments(path)
23
+ parser = BetterHtml::Parser.new(make_buffer(path))
24
+ ast = convert_better_html(parser.ast)
25
+ @erb_ast_processor.process_and_extract_comments(ast)
26
+ end
27
+
28
+ # Convert BetterHtml nodes to Parser::AST::Node
29
+ #
30
+ # @param node BetterHtml::Parser::AST::Node
31
+ # @return Parser::AST::Node
32
+ def convert_better_html(node)
33
+ definition = Parser::Source::Map::Definition.new(
34
+ node.location.begin,
35
+ node.location.begin,
36
+ node.location.begin,
37
+ node.location.end
38
+ )
39
+ Parser::AST::Node.new(
40
+ node.type,
41
+ node.children.map { |child| child.is_a?(BetterHtml::AST::Node) ? convert_better_html(child) : child },
42
+ {
43
+ location: definition
44
+ }
45
+ )
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ module I18n::Tasks::Scanners
6
+ class LocalRubyParser
7
+ # ignore_blocks feature inspired by shopify/better-html
8
+ # https://github.com/Shopify/better-html/blob/087943ffd2a5877fa977d71532010b0c91239519/lib/better_html/test_helper/ruby_node.rb#L24
9
+ BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/.freeze
10
+
11
+ def initialize(ignore_blocks: false)
12
+ @parser = ::Parser::CurrentRuby.new
13
+ @ignore_blocks = ignore_blocks
14
+ end
15
+
16
+ # Parse string and normalize location
17
+ def parse(source, location: nil)
18
+ buffer = ::Parser::Source::Buffer.new('(string)')
19
+ buffer.source = if @ignore_blocks
20
+ source.sub(BLOCK_EXPR, '')
21
+ else
22
+ source
23
+ end
24
+
25
+ @parser.reset
26
+ ast, comments = @parser.parse_with_comments(buffer)
27
+ ast = normalize_location(ast, location)
28
+ comments = comments.map { |comment| normalize_comment_location(comment, location) }
29
+ [ast, comments]
30
+ end
31
+
32
+ # Normalize location for all parsed nodes
33
+
34
+ # @param node {Parser::AST::Node} Node in parsed code
35
+ # @param location {Parser::Source::Map} Global location for the parsed string
36
+ # @return {Parser::AST::Node}
37
+ def normalize_location(node, location)
38
+ return node.map { |child| normalize_location(child, location) } if node.is_a?(Array)
39
+
40
+ return node unless node.is_a?(::Parser::AST::Node)
41
+
42
+ node.updated(
43
+ nil,
44
+ node.children.map { |child| normalize_location(child, location) },
45
+ { location: updated_location(location, node.location) }
46
+ )
47
+ end
48
+
49
+ # Calculate location relative to a global location
50
+ #
51
+ # @param global_location {Parser::Source::Map} Global location where the code was parsed
52
+ # @param local_location {Parser::Source::Map} Local location in the parsed string
53
+ # @return {Parser::Source::Map}
54
+ def updated_location(global_location, local_location)
55
+ return global_location if local_location.expression.nil?
56
+
57
+ range = ::Parser::Source::Range.new(
58
+ global_location.expression.source_buffer,
59
+ global_location.expression.to_range.begin + local_location.expression.to_range.begin,
60
+ global_location.expression.to_range.begin + local_location.expression.to_range.end
61
+ )
62
+
63
+ ::Parser::Source::Map::Definition.new(
64
+ range.begin,
65
+ range.begin,
66
+ range.begin,
67
+ range.end
68
+ )
69
+ end
70
+
71
+ # Normalize location for comment
72
+ #
73
+ # @param comment {Parser::Source::Comment} A comment with local location
74
+ # @param location {Parser::Source::Map} Global location for the parsed string
75
+ # @return {Parser::Source::Comment}
76
+ def normalize_comment_location(comment, location)
77
+ range = ::Parser::Source::Range.new(
78
+ location.expression.source_buffer,
79
+ location.expression.to_range.begin + comment.location.expression.to_range.begin,
80
+ location.expression.to_range.begin + comment.location.expression.to_range.end
81
+ )
82
+ ::Parser::Source::Comment.new(range)
83
+ end
84
+ end
85
+ end
@@ -35,7 +35,7 @@ module I18n::Tasks::Scanners
35
35
  result = []
36
36
  text.scan(pattern) do |_|
37
37
  match = Regexp.last_match
38
- matches = Hash[match.names.map(&:to_sym).zip(match.captures)]
38
+ matches = match.names.map(&:to_sym).zip(match.captures).to_h
39
39
  if matches.key?(:key)
40
40
  matches[:key] = strip_literal(matches[:key])
41
41
  next unless valid_key?(matches[:key])
@@ -12,7 +12,7 @@ module I18n::Tasks::Scanners
12
12
  include OccurrenceFromPosition
13
13
  include RubyKeyLiterals
14
14
 
15
- TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
15
+ TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'-]I18n\.|I18n\.)t(?:!|ranslate!?)?/.freeze
16
16
  IGNORE_LINES = {
17
17
  'coffee' => /^\s*#(?!\si18n-tasks-use)/,
18
18
  'erb' => /^\s*<%\s*#(?!\si18n-tasks-use)/,
@@ -66,7 +66,7 @@ module I18n::Tasks::Scanners
66
66
  end
67
67
 
68
68
  def exclude_line?(line, path)
69
- re = @ignore_lines_res[File.extname(path)[1..-1]]
69
+ re = @ignore_lines_res[File.extname(path)[1..]]
70
70
  re && re =~ line
71
71
  end
72
72
 
@@ -10,7 +10,7 @@ module I18n
10
10
  # @param calling_method [#call, Symbol, String, false, nil]
11
11
  # @return [String] absolute version of the key
12
12
  def absolute_key(key, path, roots: config[:relative_roots],
13
- exclude_method_name_paths: config[:exclude_method_name_paths],
13
+ exclude_method_name_paths: config[:relative_exclude_method_name_paths],
14
14
  calling_method: nil)
15
15
  return key unless key.start_with?(DOT)
16
16
  fail 'roots argument is required' unless roots.present?
@@ -18,7 +18,7 @@ module I18n
18
18
  normalized_path = File.expand_path(path)
19
19
  (root = path_root(normalized_path, roots)) ||
20
20
  fail(CommandError, "Cannot resolve relative key \"#{key}\".\n" \
21
- "Set search.relative_roots in config/i18n-tasks.yml (currently #{roots.inspect})")
21
+ "Set search.relative_roots in config/i18n-tasks.yml (currently #{roots.inspect})")
22
22
  normalized_path.sub!(root, '')
23
23
 
24
24
  if (exclude_method_name_paths || []).map { |p| expand_path(p) }.include?(root)
@@ -48,7 +48,7 @@ module I18n::Tasks
48
48
  # rubocop:enable Metrics/ParameterLists
49
49
 
50
50
  def inspect
51
- "Occurrence(#{@path}:#{@line_num}:#{@line_pos}:#{@pos}:#{@raw_key}:#{@default_arg})"
51
+ "Occurrence(#{@path}:#{@line_num}, line_pos: #{@line_pos}, pos: #{@pos}, raw_key: #{@raw_key}, default_arg: #{@default_arg}, line: #{@line})" # rubocop:disable Layout/LineLength
52
52
  end
53
53
 
54
54
  def ==(other)
@@ -63,6 +63,22 @@ module I18n::Tasks
63
63
  def hash
64
64
  [@path, @pos, @line_num, @line_pos, @line, @default_arg].hash
65
65
  end
66
+
67
+ # @param raw_key [String]
68
+ # @param range [Parser::Source::Range]
69
+ # @param default_arg [String, nil]
70
+ # @return [Results::Occurrence]
71
+ def self.from_range(raw_key:, range:, default_arg: nil)
72
+ Occurrence.new(
73
+ path: range.source_buffer.name,
74
+ pos: range.begin_pos,
75
+ line_num: range.line,
76
+ line_pos: range.column,
77
+ line: range.source_line,
78
+ raw_key: raw_key,
79
+ default_arg: default_arg
80
+ )
81
+ end
66
82
  end
67
83
  end
68
84
  end