i18n-tasks 0.9.37 → 1.0.2
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/README.md +3 -1
- data/i18n-tasks.gemspec +1 -0
- data/lib/i18n/tasks/command/commander.rb +1 -0
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +9 -2
- data/lib/i18n/tasks/scanners/erb_ast_processor.rb +51 -0
- data/lib/i18n/tasks/scanners/erb_ast_scanner.rb +48 -0
- data/lib/i18n/tasks/scanners/local_ruby_parser.rb +83 -0
- data/lib/i18n/tasks/scanners/results/occurrence.rb +1 -1
- data/lib/i18n/tasks/scanners/ruby_ast_call_finder.rb +1 -0
- data/lib/i18n/tasks/scanners/ruby_ast_scanner.rb +46 -21
- data/lib/i18n/tasks/used_keys.rb +3 -1
- data/lib/i18n/tasks/version.rb +1 -1
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66781586bda6c38de2ca767c265c7e1fbaf6e11184997f00a77c215e2270a7d7
|
4
|
+
data.tar.gz: 3a94d73244c34887758d208cdc807a930467ac6cde95545dea6acdbed5563c36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b51f3bf32fc208a9845b79f9a074ed2bcd2f0ff648309b93dbf7431df7154103a63c298bdb197cfae611012423d315df1552ad600a20135dfd76592df5d853d8
|
7
|
+
data.tar.gz: 4cc284d8c283c4159333310f4deb2e78ec064f60c024ccdadd4007d883955ca08d1e765b69529744dff2f953973d3cab6ff46138919c5cc41c3c4f986ea2a88a
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# i18n-tasks [![Build Status][badge-ci]][ci] [![Coverage Status][badge-coverage]][coverage] [](https://gitter.im/glebm/i18n-tasks?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
2
2
|
|
3
|
+
[](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.
|
27
|
+
gem 'i18n-tasks', '~> 1.0.2'
|
26
28
|
```
|
27
29
|
|
28
30
|
Copy the default [configuration file](#configuration):
|
data/i18n-tasks.gemspec
CHANGED
@@ -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.
|
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(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
|
+
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,83 @@
|
|
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
|
+
range = ::Parser::Source::Range.new(
|
56
|
+
global_location.expression.source_buffer,
|
57
|
+
global_location.expression.to_range.begin + local_location.expression.to_range.begin,
|
58
|
+
global_location.expression.to_range.begin + local_location.expression.to_range.end
|
59
|
+
)
|
60
|
+
|
61
|
+
::Parser::Source::Map::Definition.new(
|
62
|
+
range.begin,
|
63
|
+
range.begin,
|
64
|
+
range.begin,
|
65
|
+
range.end
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Normalize location for comment
|
70
|
+
#
|
71
|
+
# @param comment {Parser::Source::Comment} A comment with local location
|
72
|
+
# @param location {Parser::Source::Map} Global location for the parsed string
|
73
|
+
# @return {Parser::Source::Comment}
|
74
|
+
def normalize_comment_location(comment, location)
|
75
|
+
range = ::Parser::Source::Range.new(
|
76
|
+
location.expression.source_buffer,
|
77
|
+
location.expression.to_range.begin + comment.location.expression.to_range.begin,
|
78
|
+
location.expression.to_range.begin + comment.location.expression.to_range.end
|
79
|
+
)
|
80
|
+
::Parser::Source::Comment.new(range)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -48,7 +48,7 @@ module I18n::Tasks
|
|
48
48
|
# rubocop:enable Metrics/ParameterLists
|
49
49
|
|
50
50
|
def inspect
|
51
|
-
"Occurrence(#{@path}
|
51
|
+
"Occurrence(path: #{@path}, line_num: #{@line_num}, line_pos: #{@line_pos}, pos: #{@pos}, raw_key: #{@raw_key}, default_arg: #{@default_arg}, line: #{@line})"
|
52
52
|
end
|
53
53
|
|
54
54
|
def ==(other)
|
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/i18n/tasks/used_keys.rb
CHANGED
@@ -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::
|
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
|
data/lib/i18n/tasks/version.rb
CHANGED
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.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- glebm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
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
|