Skip to content
Open
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
14 changes: 13 additions & 1 deletion lib/ruby_lsp/ruby_lsp_rails/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ def workspace_did_change_watched_files(changes)

offer_to_run_pending_migrations
end

if changes.any? { |c| %r{config/locales/.*\.yml}.match?(c[:uri]) }
@rails_runner_client.trigger_i18n_reload
end
end

# @override
Expand Down Expand Up @@ -230,7 +234,7 @@ def register_additional_file_watchers(global_state:, outgoing_queue:)
id: "workspace/didChangeWatchedFilesRails",
method: "workspace/didChangeWatchedFiles",
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
watchers: [structure_sql_file_watcher, fixture_file_watcher],
watchers: [structure_sql_file_watcher, fixture_file_watcher, i18n_file_watcher],
),
),
],
Expand All @@ -254,6 +258,14 @@ def fixture_file_watcher
)
end

#: -> Interface::FileSystemWatcher
def i18n_file_watcher
Interface::FileSystemWatcher.new(
glob_pattern: "**/config/locales/**/*.{yml,yaml}",
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
)
end

#: -> void
def offer_to_run_pending_migrations
return unless @outgoing_queue
Expand Down
41 changes: 41 additions & 0 deletions lib/ruby_lsp/ruby_lsp_rails/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def initialize(client, response_builder, node_context, global_state, dispatcher)
:on_constant_path_node_enter,
:on_constant_read_node_enter,
:on_symbol_node_enter,
:on_string_node_enter,
)
end

Expand Down Expand Up @@ -56,6 +57,11 @@ def on_symbol_node_enter(node)
handle_possible_dsl(node)
end

#: (Prism::StringNode node) -> void
def on_string_node_enter(node)
handle_possible_i18n(node)
end

private

#: (String name) -> void
Expand Down Expand Up @@ -148,6 +154,31 @@ def handle_association(node)
generate_hover(result[:name])
end

#: (Prism::StringNode node) -> void
def handle_possible_i18n(node)
call_node = @node_context.call_node
return unless call_node

receiver = call_node.receiver
return unless receiver.is_a?(Prism::ConstantReadNode)
return unless receiver.name == :I18n

message = call_node.message
return unless message == "t"

first_argument = call_node.arguments&.arguments&.first
return unless first_argument.is_a?(Prism::StringNode)
return unless first_argument == node

i18n_key = first_argument.unescaped
return if i18n_key.empty?

result = @client.i18n(i18n_key)
return unless result

generate_i18n_hover(result)
end

# Copied from `RubyLsp::Listeners::Hover#generate_hover`
#: (String name) -> void
def generate_hover(name)
Expand All @@ -164,6 +195,16 @@ def generate_hover(name)
@response_builder.push(content, category: category)
end
end

#: (Hash[Symbol, String] translations) -> void
def generate_i18n_hover(translations)
content = translations.map { |lang, translation| "#{lang}: #{translation}" }.join("\n")
content = "```yaml\n#{content}\n```"
@response_builder.push(
content,
category: :documentation,
)
end
end
end
end
23 changes: 23 additions & 0 deletions lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,17 @@ def route(controller:, action:)
nil
end

#: (String key) -> Hash[Symbol, untyped]?
def i18n(key)
make_request("i18n", key: key)
rescue MessageError
log_message(
"Ruby LSP Rails failed to get i18n information",
type: RubyLsp::Constant::MessageType::ERROR,
)
nil
end

# Delegates a notification to a server add-on
#: (server_addon_name: String, request_name: String, **untyped params) -> void
def delegate_notification(server_addon_name:, request_name:, **params)
Expand Down Expand Up @@ -239,6 +250,18 @@ def trigger_reload
nil
end

#: -> void
def trigger_i18n_reload
log_message("Reloading I18n translations")
send_notification("reload_i18n")
rescue MessageError
log_message(
"Ruby LSP Rails failed to trigger I18n reload",
type: RubyLsp::Constant::MessageType::ERROR,
)
nil
end

#: -> void
def shutdown
log_message("Ruby LSP Rails shutting down server")
Expand Down
21 changes: 21 additions & 0 deletions lib/ruby_lsp/ruby_lsp_rails/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,17 @@ def execute(request, params)
with_request_error_handling(request) do
send_result(resolve_route_info(params))
end
when "i18n"
with_request_error_handling(request) do
result = resolve_i18n_key(params.fetch(:key))
send_result(result)
end
when "reload_i18n"
with_progress("rails-reload-i18n", "Reloading Ruby LSP Rails I18n") do
with_notification_error_handling(request) do
I18n.reload! if defined?(I18n) && I18n.respond_to?(:reload!)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is provided because other I18n backend maybe does not implement the reloading feature.

end
end
when "server_addon/register"
with_notification_error_handling(request) do
require params[:server_addon_path]
Expand Down Expand Up @@ -525,6 +536,16 @@ def database_supports_indexing?(model)
rescue NotImplementedError
@database_supports_indexing = false
end

#: (String) -> Hash[Symbol, String]
def resolve_i18n_key(key)
locales = I18n.available_locales
result = {}
locales.each.map do |locale|
result[locale] = I18n.t(key, locale: locale, default: "⚠️ translation missing")
end
result
end
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions test/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ class User < ApplicationRecord
def foo
puts "test"
end

def hello
I18n.t("hello")
end
end
3 changes: 3 additions & 0 deletions test/dummy/config/locales/es.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
es:
hello: "Hola mundo"
3 changes: 3 additions & 0 deletions test/dummy/config/locales/fr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
fr:
hello: "Bonjour le monde"
22 changes: 22 additions & 0 deletions test/ruby_lsp_rails/hover_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,28 @@ class Bar < ApplicationRecord
CONTENT
end

test "returns I18n translation information" do
expected_response = {
en: "hello",
es: "hola",
fr: "bonjour",
}

RunnerClient.any_instance.stubs(i18n: expected_response)

response = hover_on_source(<<~RUBY, { line: 0, character: 9 })
I18n.t("foo")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be just t if we consider that you could be inside Action View, but perhaps that is beyond the scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its out of scope. This changes only support I18n.t("some-key") calls. With absolute key and string literal as argument.

To support t notation for ActionView, we need the context of which file is beign parsed and be able to complete the call of I18n in an ActionView context, or with the scope of that specific file. It could be a nice follow up if you want to help 👍🏻 .

Copy link

@janko janko Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we need anything else other than the template filename to resolve a relative translation? I understand that you want to start with the simplest use case, I was just curious if we'd actually need the Action View context.

RUBY

assert_equal(<<~CONTENT.chomp, response.contents.value)
```yaml
en: hello
es: hola
fr: bonjour
```
CONTENT
end

private

def hover_on_source(source, position)
Expand Down
18 changes: 18 additions & 0 deletions test/ruby_lsp_rails/runner_client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ class RunnerClientTest < ActiveSupport::TestCase
assert_match(%r{db/schema\.rb$}, response.fetch(:schema_file))
end

test "#i18n returns translations for the requested key" do
RunnerClient.any_instance.stubs(model: "hello")
translations = @client.i18n("hello") #: as !nil
assert_instance_of(Hash, translations)
assert_equal("Hello world", translations[:en])
assert_equal("Hola mundo", translations[:es])
assert_equal("Bonjour le monde", translations[:fr])
end

test "#i18n returns translation missing for a key" do
RunnerClient.any_instance.stubs(model: "hello")
translations = @client.i18n("missing_key") #: as !nil
assert_instance_of(Hash, translations)
assert_equal("⚠️ translation missing", translations[:en])
assert_equal("⚠️ translation missing", translations[:es])
assert_equal("⚠️ translation missing", translations[:fr])
end

test "returns nil if the request returns a nil response" do
assert_nil @client.model("ApplicationRecord") # ApplicationRecord is abstract
end
Expand Down
Loading