Skip to content

Commit

Permalink
PrismScanner: Contextual parsing for Rails (#565)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwessman committed Jun 10, 2024
1 parent 2145066 commit 6a46c61
Show file tree
Hide file tree
Showing 10 changed files with 1,384 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Uses AST-parser for all ERB-files, not just `.html.erb`
* [Fixed regex in `PatternScanner`] (https://github.com/glebm/i18n-tasks/issues/572)
* Adds contextual parser to support more Rails-translations
[#565](https://github.com/glebm/i18n-tasks/pull/565)

## v1.0.14

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,30 @@ OPENAI_API_KEY=<OpenAI API key>
OPENAI_MODEL=<optional>
```

### Contextual Rails Parser

There is an experimental feature to parse Rails with more context. `i18n-tasks` will support:
- Translations called in `before_actions`
- Translations called in nested methods
- `Model.human_attribute_name` calls
- `Model.model_name.human` calls

Enabled it by adding the scanner in your `config/i18n-tasks.yml`:

```ruby
<% I18n::Tasks.add_scanner(
'I18n::Tasks::Scanners::PrismScanner',
only: %w(*.rb)
) %>
```

To only enable Ruby-scanning and not any Rails support, please add config under the `search` section:

```yaml
search:
prism_visitor: "ruby" # default "rails"
```

## Interactive console

`i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
Expand Down
71 changes: 71 additions & 0 deletions lib/i18n/tasks/scanners/prism_scanner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require_relative 'file_scanner'
require_relative 'ruby_ast_scanner'

module I18n::Tasks::Scanners
class PrismScanner < FileScanner
def initialize(**args)
unless RAILS_VISITOR || RUBY_VISITOR
warn(
'Please make sure `prism` is available to use this feature. Fallback to Ruby AST Scanner.'
)
end
super

@visitor_class = config[:prism_visitor] == 'ruby' ? RUBY_VISITOR : RAILS_VISITOR
@fallback = RubyAstScanner.new(**args)
end

protected

# Extract all occurrences of translate calls from the file at the given path.
#
# @return [Array<[key, Results::KeyOccurrence]>] each occurrence found in the file
def scan_file(path)
return @fallback.send(:scan_file, path) if @visitor_class.nil?

process_prism_parse_result(
path,
PARSER.parse_file(path).value,
PARSER.parse_file_comments(path)
)
rescue Exception => e # rubocop:disable Lint/RescueException
raise(
::I18n::Tasks::CommandError.new(
e,
"Error scanning #{path}: #{e.message}"
)
)
end

def process_prism_parse_result(path, parsed, comments = nil)
return @fallback.send(:scan_file, path) if RUBY_VISITOR.skip_prism_comment?(comments)

visitor = @visitor_class.new(comments: comments)
nodes = parsed.accept(visitor)

nodes
.filter_map do |node|
next node.occurrences(path) if node.is_a?(I18n::Tasks::Scanners::PrismScanners::TranslationNode)
next unless node.respond_to?(:translation_nodes)

node.translation_nodes.flat_map { |n| n.occurrences(path) }
end
.flatten(1)
end

# This block handles adding a fallback if the `prism` gem is not available.
begin
require 'prism'
require_relative 'prism_scanners/rails_visitor'
require_relative 'prism_scanners/visitor'
PARSER = Prism
RUBY_VISITOR = I18n::Tasks::Scanners::PrismScanners::Visitor
RAILS_VISITOR = I18n::Tasks::Scanners::PrismScanners::RailsVisitor
rescue LoadError
PARSER = nil
RUBY_VISITOR, RAILS_VISITOR = nil
end
end
end
Loading

0 comments on commit 6a46c61

Please sign in to comment.