Skip to content

Commit c4116f4

Browse files
authored
Merge pull request #326 from fatkodima/i18n_lazy_lookup-cop
Add new `Rails/I18nLazyLookup` cop
2 parents ea7a891 + eed8c42 commit c4116f4

File tree

5 files changed

+219
-0
lines changed

5 files changed

+219
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#326](https://github.com/rubocop/rubocop-rails/pull/326): Add new `Rails/I18nLazyLookup` cop. ([@fatkodima][])

config/default.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ Rails/HttpStatus:
420420
- numeric
421421
- symbolic
422422

423+
Rails/I18nLazyLookup:
424+
Description: 'Checks for places where I18n "lazy" lookup can be used.'
425+
StyleGuide: 'https://rails.rubystyle.guide/#lazy-lookup'
426+
Reference: 'https://guides.rubyonrails.org/i18n.html#lazy-lookup'
427+
Enabled: pending
428+
VersionAdded: '<<next>>'
429+
Include:
430+
- 'controllers/**/*'
431+
423432
Rails/I18nLocaleAssignment:
424433
Description: 'Prefer the usage of `I18n.with_locale` instead of manually updating `I18n.locale` value.'
425434
Enabled: 'pending'
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# This cop checks for places where I18n "lazy" lookup can be used.
7+
#
8+
# @example
9+
# # en.yml
10+
# # en:
11+
# # books:
12+
# # create:
13+
# # success: Book created!
14+
#
15+
# # bad
16+
# class BooksController < ApplicationController
17+
# def create
18+
# # ...
19+
# redirect_to books_url, notice: t('books.create.success')
20+
# end
21+
# end
22+
#
23+
# # good
24+
# class BooksController < ApplicationController
25+
# def create
26+
# # ...
27+
# redirect_to books_url, notice: t('.success')
28+
# end
29+
# end
30+
#
31+
class I18nLazyLookup < Base
32+
include VisibilityHelp
33+
extend AutoCorrector
34+
35+
MSG = 'Use "lazy" lookup for the text used in controllers.'
36+
37+
def_node_matcher :translate_call?, <<~PATTERN
38+
(send nil? {:translate :t} ${sym_type? str_type?} ...)
39+
PATTERN
40+
41+
def on_send(node)
42+
translate_call?(node) do |key_node|
43+
key = key_node.value
44+
return if key.to_s.start_with?('.')
45+
46+
controller, action = controller_and_action(node)
47+
return unless controller && action
48+
49+
scoped_key = get_scoped_key(key_node, controller, action)
50+
return unless key == scoped_key
51+
52+
add_offense(key_node) do |corrector|
53+
unscoped_key = key_node.value.to_s.split('.').last
54+
corrector.replace(key_node, "'.#{unscoped_key}'")
55+
end
56+
end
57+
end
58+
59+
private
60+
61+
def controller_and_action(node)
62+
action_node = node.each_ancestor(:def).first
63+
return unless action_node && node_visibility(action_node) == :public
64+
65+
controller_node = node.each_ancestor(:class).first
66+
return unless controller_node && controller_node.identifier.source.end_with?('Controller')
67+
68+
[controller_node, action_node]
69+
end
70+
71+
def get_scoped_key(key_node, controller, action)
72+
path = controller_path(controller).tr('/', '.')
73+
action_name = action.method_name
74+
key = key_node.value.to_s.split('.').last
75+
76+
"#{path}.#{action_name}.#{key}"
77+
end
78+
79+
def controller_path(controller)
80+
module_name = controller.parent_module_name
81+
controller_name = controller.identifier.source
82+
83+
path = if module_name == 'Object'
84+
controller_name
85+
else
86+
"#{module_name}::#{controller_name}"
87+
end
88+
89+
path.delete_suffix('Controller').underscore
90+
end
91+
end
92+
end
93+
end
94+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
require_relative 'rails/helper_instance_variable'
5151
require_relative 'rails/http_positional_arguments'
5252
require_relative 'rails/http_status'
53+
require_relative 'rails/i18n_lazy_lookup'
5354
require_relative 'rails/i18n_locale_assignment'
5455
require_relative 'rails/ignored_skip_action_filter_option'
5556
require_relative 'rails/index_by'
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::I18nLazyLookup, :config do
4+
it 'registers an offense and corrects when using translation helpers with the key scoped to controller and action' do
5+
expect_offense(<<~RUBY)
6+
class FooController
7+
def action
8+
t 'foo.action.key'
9+
^^^^^^^^^^^^^^^^ Use "lazy" lookup for the text used in controllers.
10+
translate 'foo.action.key'
11+
^^^^^^^^^^^^^^^^ Use "lazy" lookup for the text used in controllers.
12+
end
13+
end
14+
RUBY
15+
16+
expect_correction(<<~RUBY)
17+
class FooController
18+
def action
19+
t '.key'
20+
translate '.key'
21+
end
22+
end
23+
RUBY
24+
end
25+
26+
it 'does not register an offense when translation methods scoped to `I18n`' do
27+
expect_no_offenses(<<~RUBY)
28+
class FooController
29+
def action
30+
I18n.t 'foo.action.key'
31+
I18n.translate 'foo.action.key'
32+
end
33+
end
34+
RUBY
35+
end
36+
37+
it 'does not register an offense when not inside controller' do
38+
expect_no_offenses(<<~RUBY)
39+
class FooService
40+
def do_something
41+
t 'foo_service.do_something.key'
42+
end
43+
end
44+
RUBY
45+
end
46+
47+
it 'does not register an offense when not inside controller action' do
48+
expect_no_offenses(<<~RUBY)
49+
class FooController
50+
private
51+
52+
def action
53+
t 'foo.action.key'
54+
end
55+
end
56+
RUBY
57+
end
58+
59+
it 'does not register an offense when translating key not scoped to controller and action' do
60+
expect_no_offenses(<<~RUBY)
61+
class FooController
62+
def action
63+
t 'one.two.key'
64+
end
65+
end
66+
RUBY
67+
end
68+
69+
it 'does not register an offense when using "lazy" translation' do
70+
expect_no_offenses(<<~RUBY)
71+
class FooController
72+
def action
73+
t '.key'
74+
end
75+
end
76+
RUBY
77+
end
78+
79+
it 'does not register an offense when translation key is not a string nor a symbol' do
80+
expect_no_offenses(<<~RUBY)
81+
class FooController
82+
def action
83+
t ['foo.action.key']
84+
t key
85+
end
86+
end
87+
RUBY
88+
end
89+
90+
it 'handles scoped controllers' do
91+
expect_offense(<<~RUBY)
92+
module Bar
93+
class FooController
94+
def action
95+
t 'bar.foo.action.key'
96+
^^^^^^^^^^^^^^^^^^^^ Use "lazy" lookup for the text used in controllers.
97+
t 'foo.action.key'
98+
end
99+
end
100+
end
101+
RUBY
102+
103+
expect_correction(<<~RUBY)
104+
module Bar
105+
class FooController
106+
def action
107+
t '.key'
108+
t 'foo.action.key'
109+
end
110+
end
111+
end
112+
RUBY
113+
end
114+
end

0 commit comments

Comments
 (0)