diff --git a/changelog/new_add_new_i18n_lazy_lookup_cop.md b/changelog/new_add_new_i18n_lazy_lookup_cop.md new file mode 100644 index 0000000000..53d6a438ef --- /dev/null +++ b/changelog/new_add_new_i18n_lazy_lookup_cop.md @@ -0,0 +1 @@ +* [#326](https://github.com/rubocop/rubocop-rails/pull/326): Add new `Rails/I18nLazyLookup` cop. ([@fatkodima][]) diff --git a/config/default.yml b/config/default.yml index 6c58d9032c..446672b277 100644 --- a/config/default.yml +++ b/config/default.yml @@ -420,6 +420,15 @@ Rails/HttpStatus: - numeric - symbolic +Rails/I18nLazyLookup: + Description: 'Checks for places where I18n "lazy" lookup can be used.' + StyleGuide: 'https://rails.rubystyle.guide/#lazy-lookup' + Reference: 'https://guides.rubyonrails.org/i18n.html#lazy-lookup' + Enabled: pending + VersionAdded: '<>' + Include: + - 'controllers/**/*' + Rails/I18nLocaleAssignment: Description: 'Prefer the usage of `I18n.with_locale` instead of manually updating `I18n.locale` value.' Enabled: 'pending' diff --git a/lib/rubocop/cop/rails/i18n_lazy_lookup.rb b/lib/rubocop/cop/rails/i18n_lazy_lookup.rb new file mode 100644 index 0000000000..d3093ea23d --- /dev/null +++ b/lib/rubocop/cop/rails/i18n_lazy_lookup.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Rails + # This cop checks for places where I18n "lazy" lookup can be used. + # + # @example + # # en.yml + # # en: + # # books: + # # create: + # # success: Book created! + # + # # bad + # class BooksController < ApplicationController + # def create + # # ... + # redirect_to books_url, notice: t('books.create.success') + # end + # end + # + # # good + # class BooksController < ApplicationController + # def create + # # ... + # redirect_to books_url, notice: t('.success') + # end + # end + # + class I18nLazyLookup < Base + include VisibilityHelp + extend AutoCorrector + + MSG = 'Use "lazy" lookup for the text used in controllers.' + + def_node_matcher :translate_call?, <<~PATTERN + (send nil? {:translate :t} ${sym_type? str_type?} ...) + PATTERN + + def on_send(node) + translate_call?(node) do |key_node| + key = key_node.value + return if key.to_s.start_with?('.') + + controller, action = controller_and_action(node) + return unless controller && action + + scoped_key = get_scoped_key(key_node, controller, action) + return unless key == scoped_key + + add_offense(key_node) do |corrector| + unscoped_key = key_node.value.to_s.split('.').last + corrector.replace(key_node, "'.#{unscoped_key}'") + end + end + end + + private + + def controller_and_action(node) + action_node = node.each_ancestor(:def).first + return unless action_node && node_visibility(action_node) == :public + + controller_node = node.each_ancestor(:class).first + return unless controller_node && controller_node.identifier.source.end_with?('Controller') + + [controller_node, action_node] + end + + def get_scoped_key(key_node, controller, action) + path = controller_path(controller).tr('/', '.') + action_name = action.method_name + key = key_node.value.to_s.split('.').last + + "#{path}.#{action_name}.#{key}" + end + + def controller_path(controller) + module_name = controller.parent_module_name + controller_name = controller.identifier.source + + path = if module_name == 'Object' + controller_name + else + "#{module_name}::#{controller_name}" + end + + path.delete_suffix('Controller').underscore + end + end + end + end +end diff --git a/lib/rubocop/cop/rails_cops.rb b/lib/rubocop/cop/rails_cops.rb index fb76b5d6a5..31404629a9 100644 --- a/lib/rubocop/cop/rails_cops.rb +++ b/lib/rubocop/cop/rails_cops.rb @@ -50,6 +50,7 @@ require_relative 'rails/helper_instance_variable' require_relative 'rails/http_positional_arguments' require_relative 'rails/http_status' +require_relative 'rails/i18n_lazy_lookup' require_relative 'rails/i18n_locale_assignment' require_relative 'rails/ignored_skip_action_filter_option' require_relative 'rails/index_by' diff --git a/spec/rubocop/cop/rails/i18n_lazy_lookup_spec.rb b/spec/rubocop/cop/rails/i18n_lazy_lookup_spec.rb new file mode 100644 index 0000000000..30f0a4b597 --- /dev/null +++ b/spec/rubocop/cop/rails/i18n_lazy_lookup_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Rails::I18nLazyLookup, :config do + it 'registers an offense and corrects when using translation helpers with the key scoped to controller and action' do + expect_offense(<<~RUBY) + class FooController + def action + t 'foo.action.key' + ^^^^^^^^^^^^^^^^ Use "lazy" lookup for the text used in controllers. + translate 'foo.action.key' + ^^^^^^^^^^^^^^^^ Use "lazy" lookup for the text used in controllers. + end + end + RUBY + + expect_correction(<<~RUBY) + class FooController + def action + t '.key' + translate '.key' + end + end + RUBY + end + + it 'does not register an offense when translation methods scoped to `I18n`' do + expect_no_offenses(<<~RUBY) + class FooController + def action + I18n.t 'foo.action.key' + I18n.translate 'foo.action.key' + end + end + RUBY + end + + it 'does not register an offense when not inside controller' do + expect_no_offenses(<<~RUBY) + class FooService + def do_something + t 'foo_service.do_something.key' + end + end + RUBY + end + + it 'does not register an offense when not inside controller action' do + expect_no_offenses(<<~RUBY) + class FooController + private + + def action + t 'foo.action.key' + end + end + RUBY + end + + it 'does not register an offense when translating key not scoped to controller and action' do + expect_no_offenses(<<~RUBY) + class FooController + def action + t 'one.two.key' + end + end + RUBY + end + + it 'does not register an offense when using "lazy" translation' do + expect_no_offenses(<<~RUBY) + class FooController + def action + t '.key' + end + end + RUBY + end + + it 'does not register an offense when translation key is not a string nor a symbol' do + expect_no_offenses(<<~RUBY) + class FooController + def action + t ['foo.action.key'] + t key + end + end + RUBY + end + + it 'handles scoped controllers' do + expect_offense(<<~RUBY) + module Bar + class FooController + def action + t 'bar.foo.action.key' + ^^^^^^^^^^^^^^^^^^^^ Use "lazy" lookup for the text used in controllers. + t 'foo.action.key' + end + end + end + RUBY + + expect_correction(<<~RUBY) + module Bar + class FooController + def action + t '.key' + t 'foo.action.key' + end + end + end + RUBY + end +end