i18n-tasks 0.9.36 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cde1b3151cfb69975e8512b714de8707a8ad79a7dd38ec299a282450297399f9
4
- data.tar.gz: f98141b89b24a49b163823af6351476024b1a676d0b89a781c49dc3a10ffc1a7
3
+ metadata.gz: 053f1f934ed84abbf4f86a11762c37c78cc286f8f809d3c8c5ca2918af57cb2a
4
+ data.tar.gz: 0f70179fa394de1faafc750185774d9a47f359de6b41c8542e038e8189933186
5
5
  SHA512:
6
- metadata.gz: 364b4d1d23802bfbabab3c41c7c39677f263925459f6be39c1e746e1f07221bfb487b1dbc425e8643ebacaaa20b35c3f08eb5d00fabe7237b35556a30759a67b
7
- data.tar.gz: 9c7889833088afd77d439cc3517b99f01100cb507ca329ce30b36193054c5747b98e7fae2b972d006daa6c9973376da02c8c137ff26471fe758409677d7cdc02
6
+ metadata.gz: 45934bf36576b5dfdf8e88f019779603ddcfe989c4d6ce5c3da75d07571b5097b1f06a7147663fadaef4995532d3d2661fc1733e2bf49396820202c14d33c005
7
+ data.tar.gz: 4853657f6fb46102d63d4a6f44ca0b07e3915ab7029369c815ec7530654973af30aee1184d826cc2df1a4a8c393525a3afa05005ef30aaabcf14435a66f693e6
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # i18n-tasks [![Build Status][badge-ci]][ci] [![Coverage Status][badge-coverage]][coverage] [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/glebm/i18n-tasks?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2
2
 
3
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua/)
4
+
3
5
  i18n-tasks helps you find and manage missing and unused translations.
4
6
 
5
7
  <img width="539" height="331" src="https://i.imgur.com/XZBd8l7.png">
@@ -22,7 +24,7 @@ i18n-tasks can be used with any project using the ruby [i18n gem][i18n-gem] (def
22
24
  Add i18n-tasks to the Gemfile:
23
25
 
24
26
  ```ruby
25
- gem 'i18n-tasks', '~> 0.9.36'
27
+ gem 'i18n-tasks', '~> 1.0.1'
26
28
  ```
27
29
 
28
30
  Copy the default [configuration file](#configuration):
data/i18n-tasks.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
25
25
  TEXT
26
26
  s.homepage = 'https://github.com/glebm/i18n-tasks'
27
27
  s.metadata = { 'issue_tracker' => 'https://github.com/glebm/i18n-tasks' } if s.respond_to?(:metadata=)
28
- s.required_ruby_version = '>= 2.5', '< 4.0' if s.respond_to?(:required_ruby_version=)
28
+ s.required_ruby_version = '>= 2.6', '< 4.0' if s.respond_to?(:required_ruby_version=)
29
29
 
30
30
  s.files = `git ls-files`.split($/)
31
31
  s.files -= s.files.grep(%r{^(doc/|\.|spec/)}) + %w[CHANGES.md config/i18n-tasks.yml Gemfile]
@@ -35,6 +35,7 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
35
35
 
36
36
  s.add_dependency 'activesupport', '>= 4.0.2'
37
37
  s.add_dependency 'ast', '>= 2.1.0'
38
+ s.add_dependency 'better_html', '~> 1.0'
38
39
  s.add_dependency 'erubi'
39
40
  s.add_dependency 'highline', '>= 2.0.0'
40
41
  s.add_dependency 'i18n'
@@ -16,6 +16,7 @@ module I18n::Tasks
16
16
  end
17
17
 
18
18
  def run(name, opts = {})
19
+ log_stderr "#{Rainbow('#StandWith').bg(:blue)}#{Rainbow('Ukraine').bg(:yellow)}"
19
20
  name = name.to_sym
20
21
  public_name = name.to_s.tr '_', '-'
21
22
  log_verbose "task: #{public_name}(#{opts.map { |k, v| "#{k}: #{v.inspect}" } * ', '})"
@@ -5,11 +5,13 @@ module I18n::Tasks
5
5
  module Data
6
6
  module Adapter
7
7
  module YamlAdapter
8
+ EMOJI_REGEX = /\\u[\da-f]{8}/i
9
+
8
10
  class << self
9
11
  # @return [Hash] locale tree
10
12
  def parse(str, options)
11
13
  if YAML.method(:load).arity.abs == 2
12
- YAML.load(str, **(options || {}))
14
+ YAML.safe_load(str, **(options || {}), permitted_classes: [Symbol], aliases: true)
13
15
  else
14
16
  # older jruby and rbx 2.2.7 do not accept options
15
17
  YAML.load(str)
@@ -18,7 +20,12 @@ module I18n::Tasks
18
20
 
19
21
  # @return [String]
20
22
  def dump(tree, options)
21
- tree.to_yaml(options || {})
23
+ restore_emojis(tree.to_yaml(options || {}))
24
+ end
25
+
26
+ # @return [String]
27
+ def restore_emojis(yaml)
28
+ yaml.gsub(EMOJI_REGEX) { |m| [m[-8..].to_i(16)].pack("U") }
22
29
  end
23
30
  end
24
31
  end
@@ -0,0 +1,51 @@
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
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
+ def handler_missing(node)
39
+ node.updated(
40
+ nil,
41
+ node.children.map { |child| node?(child) ? process(child) : child }
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ def node?(node)
48
+ node.is_a?(::Parser::AST::Node)
49
+ end
50
+ end
51
+ 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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ module I18n::Tasks::Scanners
6
+ class LocalRubyParser
7
+ def initialize
8
+ @parser = ::Parser::CurrentRuby.new
9
+ end
10
+
11
+ # Parse string and normalize location
12
+ def parse(source, location: nil)
13
+ buffer = ::Parser::Source::Buffer.new('(string)')
14
+ buffer.source = source
15
+
16
+ @parser.reset
17
+ ast, comments = @parser.parse_with_comments(buffer)
18
+ ast = normalize_location(ast, location)
19
+ comments = comments.map { |comment| normalize_comment_location(comment, location) }
20
+ [ast, comments]
21
+ end
22
+
23
+ # Normalize location for all parsed nodes
24
+
25
+ # @param node {Parser::AST::Node} Node in parsed code
26
+ # @param location {Parser::Source::Map} Global location for the parsed string
27
+ # @return {Parser::AST::Node}
28
+ def normalize_location(node, location)
29
+ return node.map { |child| normalize_location(child, location) } if node.is_a?(Array)
30
+
31
+ return node unless node.is_a?(::Parser::AST::Node)
32
+
33
+ node.updated(
34
+ nil,
35
+ node.children.map { |child| normalize_location(child, location) },
36
+ { location: updated_location(location, node.location) }
37
+ )
38
+ end
39
+
40
+ # Calculate location relative to a global location
41
+ #
42
+ # @param global_location {Parser::Source::Map} Global location where the code was parsed
43
+ # @param local_location {Parser::Source::Map} Local location in the parsed string
44
+ # @return {Parser::Source::Map}
45
+ def updated_location(global_location, local_location)
46
+ range = ::Parser::Source::Range.new(
47
+ global_location.expression.source_buffer,
48
+ global_location.expression.to_range.begin + local_location.expression.to_range.begin,
49
+ global_location.expression.to_range.begin + local_location.expression.to_range.end
50
+ )
51
+
52
+ ::Parser::Source::Map::Definition.new(
53
+ range.begin,
54
+ range.begin,
55
+ range.begin,
56
+ range.end
57
+ )
58
+ end
59
+
60
+ # Normalize location for comment
61
+ #
62
+ # @param comment {Parser::Source::Comment} A comment with local location
63
+ # @param location {Parser::Source::Map} Global location for the parsed string
64
+ # @return {Parser::Source::Comment}
65
+ def normalize_comment_location(comment, location)
66
+ range = ::Parser::Source::Range.new(
67
+ location.expression.source_buffer,
68
+ location.expression.to_range.begin + comment.location.expression.to_range.begin,
69
+ location.expression.to_range.begin + comment.location.expression.to_range.end
70
+ )
71
+ ::Parser::Source::Comment.new(range)
72
+ end
73
+ end
74
+ end
@@ -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}:#{@pos}:#{@raw_key}:#{@default_arg}:#{@line})"
52
52
  end
53
53
 
54
54
  def ==(other)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'ast'
4
4
  require 'set'
5
+
5
6
  module I18n::Tasks::Scanners
6
7
  class RubyAstCallFinder
7
8
  include AST::Processor::Mixin
@@ -32,32 +32,22 @@ module I18n::Tasks::Scanners
32
32
  #
33
33
  # @return [Array<[key, Results::KeyOccurrence]>] each occurrence found in the file
34
34
  def scan_file(path)
35
- @parser.reset
36
- ast, comments = @parser.parse_with_comments(make_buffer(path))
37
-
38
- results = @call_finder.collect_calls ast do |send_node, method_name|
39
- send_node_to_key_occurrence(send_node, method_name)
40
- end
35
+ ast, comments = path_to_ast_and_comments(path)
41
36
 
42
- magic_comments = comments.select { |comment| comment.text =~ MAGIC_COMMENT_PREFIX }
43
- comment_to_node = Parser::Source::Comment.associate_locations(ast, magic_comments).tap do |h|
44
- # transform_values is only available in ActiveSupport 4.2+
45
- h.each { |k, v| h[k] = v.first }
46
- end.invert
47
- results + (magic_comments.flat_map do |comment|
48
- @parser.reset
49
- associated_node = comment_to_node[comment]
50
- @call_finder.collect_calls(
51
- @parser.parse(make_buffer(path, comment.text.sub(MAGIC_COMMENT_PREFIX, '').split(/\s+(?=t)/).join('; ')))
52
- ) do |send_node, _method_name|
53
- # method_name is not available at this stage
54
- send_node_to_key_occurrence(send_node, nil, location: associated_node || comment.location)
55
- end
56
- end)
37
+ ast_to_occurences(ast) + comments_to_occurences(path, ast, comments)
57
38
  rescue Exception => e # rubocop:disable Lint/RescueException
58
39
  raise ::I18n::Tasks::CommandError.new(e, "Error scanning #{path}: #{e.message}")
59
40
  end
60
41
 
42
+ # Parse file on path and returns AST and comments.
43
+ #
44
+ # @param path Path to file to parse
45
+ # @return [{Parser::AST::Node}, [Parser::Source::Comment]]
46
+ def path_to_ast_and_comments(path)
47
+ @parser.reset
48
+ @parser.parse_with_comments(make_buffer(path))
49
+ end
50
+
61
51
  # @param send_node [Parser::AST::Node]
62
52
  # @param method_name [Symbol, nil]
63
53
  # @param location [Parser::Source::Map]
@@ -204,6 +194,41 @@ module I18n::Tasks::Scanners
204
194
  buffer.raw_source = contents
205
195
  end
206
196
  end
197
+
198
+ # Convert an array of {Parser::Source::Comment} to occurrences.
199
+ #
200
+ # @param path Path to file
201
+ # @param ast Parser::AST::Node
202
+ # @param comments [Parser::Source::Comment]
203
+ # @return [nil, [key, Occurrence]] full absolute key name and the occurrence.
204
+ def comments_to_occurences(path, ast, comments)
205
+ magic_comments = comments.select { |comment| comment.text =~ MAGIC_COMMENT_PREFIX }
206
+ comment_to_node = Parser::Source::Comment.associate_locations(ast, magic_comments).tap do |h|
207
+ # transform_values is only available in ActiveSupport 4.2+
208
+ h.each { |k, v| h[k] = v.first }
209
+ end.invert
210
+
211
+ magic_comments.flat_map do |comment|
212
+ @parser.reset
213
+ associated_node = comment_to_node[comment]
214
+ @call_finder.collect_calls(
215
+ @parser.parse(make_buffer(path, comment.text.sub(MAGIC_COMMENT_PREFIX, '').split(/\s+(?=t)/).join('; ')))
216
+ ) do |send_node, _method_name|
217
+ # method_name is not available at this stage
218
+ send_node_to_key_occurrence(send_node, nil, location: associated_node || comment.location)
219
+ end
220
+ end
221
+ end
222
+
223
+ # Convert {Parser::AST::Node} to occurrences.
224
+ #
225
+ # @param ast {Parser::Source::Comment}
226
+ # @return [nil, [key, Occurrence]] full absolute key name and the occurrence.
227
+ def ast_to_occurences(ast)
228
+ @call_finder.collect_calls(ast) do |send_node, method_name|
229
+ send_node_to_key_occurrence(send_node, method_name)
230
+ end
231
+ end
207
232
  end
208
233
  end
209
234
  # rubocop:enable Metrics/AbcSize,Metrics/BlockNesting,Metrics/PerceivedComplexity
@@ -2,12 +2,10 @@
2
2
 
3
3
  module I18n::Tasks::Scanners
4
4
  module RubyKeyLiterals
5
- # NOTE
6
- # "#{double_quoted["hash_pattern"]}" | "#{double_quoted_pattern}" | 'single_quoted_pattern' | :symbol_pattern
7
- LITERAL_RE = /:?"[^\[]+\["[^"]+"\].+"|:?".+?"|:?'.+?'|:\w+/.freeze
5
+ LITERAL_RE = /:?".+?"|:?'.+?'|:\w+/.freeze
8
6
 
9
7
  # Match literals:
10
- # * String: '', "#{}", "#{hash["key"]}"
8
+ # * String: '', "#{}"
11
9
  # * Symbol: :sym, :'', :"#{}"
12
10
  def literal_re
13
11
  LITERAL_RE
@@ -22,7 +20,7 @@ module I18n::Tasks::Scanners
22
20
  literal
23
21
  end
24
22
 
25
- VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!:;À-ž\/'"\[\]])/.freeze
23
+ VALID_KEY_CHARS = /(?:[[:word:]]|[-.?!:;À-ž\/])/.freeze
26
24
  VALID_KEY_RE = /^#{VALID_KEY_CHARS}+$/.freeze
27
25
 
28
26
  def valid_key?(key)
@@ -3,6 +3,7 @@
3
3
  require 'find'
4
4
  require 'i18n/tasks/scanners/pattern_with_scope_scanner'
5
5
  require 'i18n/tasks/scanners/ruby_ast_scanner'
6
+ require 'i18n/tasks/scanners/erb_ast_scanner'
6
7
  require 'i18n/tasks/scanners/scanner_multiplexer'
7
8
  require 'i18n/tasks/scanners/files/caching_file_finder_provider'
8
9
  require 'i18n/tasks/scanners/files/caching_file_reader'
@@ -20,7 +21,8 @@ module I18n::Tasks
20
21
  relative_roots: %w[app/controllers app/helpers app/mailers app/presenters app/views].freeze,
21
22
  scanners: [
22
23
  ['::I18n::Tasks::Scanners::RubyAstScanner', { only: %w[*.rb] }],
23
- ['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.rb] }]
24
+ ['::I18n::Tasks::Scanners::ErbAstScanner', { only: %w[*.erb] }],
25
+ ['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.erb *.rb] }]
24
26
  ],
25
27
  strict: true
26
28
  }.freeze
@@ -2,6 +2,6 @@
2
2
 
3
3
  module I18n
4
4
  module Tasks
5
- VERSION = '0.9.36'
5
+ VERSION = '1.0.1'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.36
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - glebm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-21 00:00:00.000000000 Z
11
+ date: 2022-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 2.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: better_html
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: erubi
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -357,12 +371,15 @@ files:
357
371
  - lib/i18n/tasks/references.rb
358
372
  - lib/i18n/tasks/reports/base.rb
359
373
  - lib/i18n/tasks/reports/terminal.rb
374
+ - lib/i18n/tasks/scanners/erb_ast_processor.rb
375
+ - lib/i18n/tasks/scanners/erb_ast_scanner.rb
360
376
  - lib/i18n/tasks/scanners/file_scanner.rb
361
377
  - lib/i18n/tasks/scanners/files/caching_file_finder.rb
362
378
  - lib/i18n/tasks/scanners/files/caching_file_finder_provider.rb
363
379
  - lib/i18n/tasks/scanners/files/caching_file_reader.rb
364
380
  - lib/i18n/tasks/scanners/files/file_finder.rb
365
381
  - lib/i18n/tasks/scanners/files/file_reader.rb
382
+ - lib/i18n/tasks/scanners/local_ruby_parser.rb
366
383
  - lib/i18n/tasks/scanners/occurrence_from_position.rb
367
384
  - lib/i18n/tasks/scanners/pattern_mapper.rb
368
385
  - lib/i18n/tasks/scanners/pattern_scanner.rb
@@ -406,7 +423,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
406
423
  requirements:
407
424
  - - ">="
408
425
  - !ruby/object:Gem::Version
409
- version: '2.5'
426
+ version: '2.6'
410
427
  - - "<"
411
428
  - !ruby/object:Gem::Version
412
429
  version: '4.0'