Skip to content

Commit 0e56a20

Browse files
authored
Merge pull request #640 from domingo2000/feat/add-hover-on-i18n-translations
Add hover on i18n translations
2 parents ab48a40 + f15407c commit 0e56a20

9 files changed

Lines changed: 172 additions & 1 deletion

File tree

lib/ruby_lsp/ruby_lsp_rails/addon.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ def workspace_did_change_watched_files(changes)
150150

151151
offer_to_run_pending_migrations
152152
end
153+
154+
if changes.any? { |c| %r{config/locales/.*\.yml}.match?(c[:uri]) }
155+
@rails_runner_client.trigger_i18n_reload
156+
end
153157
end
154158

155159
# @override
@@ -231,7 +235,7 @@ def register_additional_file_watchers(global_state:, outgoing_queue:)
231235
id: "workspace/didChangeWatchedFilesRails",
232236
method: "workspace/didChangeWatchedFiles",
233237
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
234-
watchers: [structure_sql_file_watcher, fixture_file_watcher],
238+
watchers: [structure_sql_file_watcher, fixture_file_watcher, i18n_file_watcher],
235239
),
236240
),
237241
],
@@ -255,6 +259,14 @@ def fixture_file_watcher
255259
)
256260
end
257261

262+
#: -> Interface::FileSystemWatcher
263+
def i18n_file_watcher
264+
Interface::FileSystemWatcher.new(
265+
glob_pattern: "**/config/locales/**/*.{yml,yaml}",
266+
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
267+
)
268+
end
269+
258270
#: -> void
259271
def offer_to_run_pending_migrations
260272
return unless @outgoing_queue

lib/ruby_lsp/ruby_lsp_rails/hover.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def initialize(client, response_builder, node_context, global_state, dispatcher)
2929
:on_constant_path_node_enter,
3030
:on_constant_read_node_enter,
3131
:on_symbol_node_enter,
32+
:on_string_node_enter,
3233
)
3334
end
3435

@@ -56,6 +57,11 @@ def on_symbol_node_enter(node)
5657
handle_possible_dsl(node)
5758
end
5859

60+
#: (Prism::StringNode node) -> void
61+
def on_string_node_enter(node)
62+
handle_possible_i18n(node)
63+
end
64+
5965
private
6066

6167
#: (String name) -> void
@@ -156,6 +162,36 @@ def handle_association(node)
156162
generate_hover(result[:name])
157163
end
158164

165+
#: (Prism::StringNode node) -> void
166+
def handle_possible_i18n(node)
167+
call_node = @node_context.call_node
168+
return unless i18n_translate?(call_node)
169+
170+
first_argument = call_node #: as !nil
171+
.arguments&.arguments&.first
172+
return unless first_argument == node
173+
174+
i18n_key = first_argument.unescaped
175+
return if i18n_key.empty?
176+
177+
result = @client.i18n(i18n_key)
178+
return unless result
179+
180+
generate_i18n_hover(result)
181+
end
182+
183+
#: (Prism::CallNode? call_node) -> bool
184+
def i18n_translate?(call_node)
185+
return false unless call_node
186+
187+
receiver = call_node.receiver
188+
return false unless receiver.is_a?(Prism::ConstantReadNode)
189+
return false unless receiver.name == :I18n
190+
191+
message = call_node.message
192+
message == "t" || message == "translate"
193+
end
194+
159195
# Copied from `RubyLsp::Listeners::Hover#generate_hover`
160196
#: (String name) -> void
161197
def generate_hover(name)
@@ -172,6 +208,16 @@ def generate_hover(name)
172208
@response_builder.push(content, category: category)
173209
end
174210
end
211+
212+
#: (Hash[Symbol, String] translations) -> void
213+
def generate_i18n_hover(translations)
214+
content = translations.map { |lang, translation| "#{lang}: #{translation}" }.join("\n")
215+
content = "```yaml\n#{content}\n```"
216+
@response_builder.push(
217+
content,
218+
category: :documentation,
219+
)
220+
end
175221
end
176222
end
177223
end

lib/ruby_lsp/ruby_lsp_rails/runner_client.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,17 @@ def route(controller:, action:)
181181
nil
182182
end
183183

184+
#: (String key) -> Hash[Symbol, untyped]?
185+
def i18n(key)
186+
make_request("i18n", key: key)
187+
rescue MessageError
188+
log_message(
189+
"Ruby LSP Rails failed to get i18n information",
190+
type: RubyLsp::Constant::MessageType::ERROR,
191+
)
192+
nil
193+
end
194+
184195
# Delegates a notification to a server add-on
185196
#: (server_addon_name: String, request_name: String, **untyped params) -> void
186197
def delegate_notification(server_addon_name:, request_name:, **params)
@@ -240,6 +251,18 @@ def trigger_reload
240251
nil
241252
end
242253

254+
#: -> void
255+
def trigger_i18n_reload
256+
log_message("Reloading I18n translations")
257+
send_notification("reload_i18n")
258+
rescue MessageError
259+
log_message(
260+
"Ruby LSP Rails failed to trigger I18n reload",
261+
type: RubyLsp::Constant::MessageType::ERROR,
262+
)
263+
nil
264+
end
265+
243266
#: -> void
244267
def shutdown
245268
log_message("Ruby LSP Rails shutting down server")

lib/ruby_lsp/ruby_lsp_rails/server.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,17 @@ def execute(request, params)
333333
with_request_error_handling(request) do
334334
send_result(resolve_route_info(params))
335335
end
336+
when "i18n"
337+
with_request_error_handling(request) do
338+
result = resolve_i18n_key(params.fetch(:key))
339+
send_result(result)
340+
end
341+
when "reload_i18n"
342+
with_progress("rails-reload-i18n", "Reloading Ruby LSP Rails I18n") do
343+
with_notification_error_handling(request) do
344+
I18n.reload! if defined?(I18n) && I18n.respond_to?(:reload!)
345+
end
346+
end
336347
when "server_addon/register"
337348
with_notification_error_handling(request) do
338349
require params[:server_addon_path]
@@ -538,6 +549,13 @@ def database_supports_indexing?(model)
538549
rescue NotImplementedError
539550
@database_supports_indexing = false
540551
end
552+
553+
#: (String) -> Hash[Symbol, String]
554+
def resolve_i18n_key(key)
555+
I18n.available_locales.each_with_object({}) do |locale, result|
556+
result[locale] = I18n.t(key, locale: locale, default: "⚠️ translation missing")
557+
end
558+
end
541559
end
542560
end
543561
end

test/dummy/app/models/user.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ class User < ApplicationRecord
1616
def foo
1717
puts "test"
1818
end
19+
20+
def hello
21+
I18n.t("hello")
22+
end
1923
end

test/dummy/config/locales/es.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
es:
3+
hello: "Hola mundo"

test/dummy/config/locales/fr.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
fr:
3+
hello: "Bonjour le monde"

test/ruby_lsp_rails/hover_test.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,50 @@ class User < ApplicationRecord
384384
CONTENT
385385
end
386386

387+
test "returns I18n translation information" do
388+
expected_response = {
389+
en: "hello",
390+
es: "hola",
391+
fr: "bonjour",
392+
}
393+
394+
RunnerClient.any_instance.stubs(i18n: expected_response)
395+
396+
response = hover_on_source(<<~RUBY, { line: 0, character: 9 })
397+
I18n.t("foo")
398+
RUBY
399+
400+
assert_equal(<<~CONTENT.chomp, response.contents.value)
401+
```yaml
402+
en: hello
403+
es: hola
404+
fr: bonjour
405+
```
406+
CONTENT
407+
end
408+
409+
test "returns I18n translation information for translate method" do
410+
expected_response = {
411+
en: "hello",
412+
es: "hola",
413+
fr: "bonjour",
414+
}
415+
416+
RunnerClient.any_instance.stubs(i18n: expected_response)
417+
418+
response = hover_on_source(<<~RUBY, { line: 0, character: 16 })
419+
I18n.translate("foo")
420+
RUBY
421+
422+
assert_equal(<<~CONTENT.chomp, response.contents.value)
423+
```yaml
424+
en: hello
425+
es: hola
426+
fr: bonjour
427+
```
428+
CONTENT
429+
end
430+
387431
private
388432

389433
def hover_on_source(source, position)

test/ruby_lsp_rails/runner_client_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ class RunnerClientTest < ActiveSupport::TestCase
6969
assert_match(%r{db/schema\.rb$}, response.fetch(:schema_file))
7070
end
7171

72+
test "#i18n returns translations for the requested key" do
73+
RunnerClient.any_instance.stubs(model: "hello")
74+
translations = @client.i18n("hello") #: as !nil
75+
assert_instance_of(Hash, translations)
76+
assert_equal("Hello world", translations[:en])
77+
assert_equal("Hola mundo", translations[:es])
78+
assert_equal("Bonjour le monde", translations[:fr])
79+
end
80+
81+
test "#i18n returns translation missing for a key" do
82+
RunnerClient.any_instance.stubs(model: "hello")
83+
translations = @client.i18n("missing_key") #: as !nil
84+
assert_instance_of(Hash, translations)
85+
assert_equal("⚠️ translation missing", translations[:en])
86+
assert_equal("⚠️ translation missing", translations[:es])
87+
assert_equal("⚠️ translation missing", translations[:fr])
88+
end
89+
7290
test "returns nil if the request returns a nil response" do
7391
assert_nil @client.model("ApplicationRecord") # ApplicationRecord is abstract
7492
end

0 commit comments

Comments
 (0)