Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PrismScanner: Contextual parsing for Rails #565

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -506,6 +506,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