-
Notifications
You must be signed in to change notification settings - Fork 143
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
I18n.locale not available to middlewares when request completes #106
Comments
It is absolutely necessary: #44 (comment) |
Alright. I'll find another solution and post it here, in case someone else would experience the same problem. Thx |
if you are able to find another solution, let me know |
@frahugo a failing spec would also be appreciated |
This is my current solution: https://gist.github.com/frahugo/705b0854126eb3ae7565 I basically push the "finale" locale (decided by ApplicationController) in the request environment, and pick it up later in the Devise middleware. |
I've run into the same issue, on an application with 2 locales, the flash alert for "Invalid email or password" is always taken from Here's a sample app to reproduce it https://github.com/jaimeiniesta/devisebug |
I tested the solution proposed by @frahugo and it works fine. |
I confirm the @frahugo fix works |
Please always pay attention to thread safety when setting the locale with a |
yes: apparently, setting I18n.locale in a Ref:
edit: maybe things have changed in the last 4 years |
Gotcha. Thanks for the fast feedback. Just one more question. The problem lies in a thread lasting longer then expected. |
How would you force that? |
Wouldn't it be forcing the value at the beginning of every request if i don't have urls without the location on them? |
Exactly how? I mean, a snippet of code that will do that. It should skip I18n.locale at all, because the issue is in how I18n.locale sets locale |
Gotcha. Thats what i expected. =) I'm doing exactly as @frahugo suggested. Since i did not have enough understanding of how Just for clarity purposes, does it mean that |
The problem in his approach is the
It is quite complicated also for me understand how Thread.current works and how threads are managed in multithreaded servers. This is explained in the stackoverflow issue I've linked and also in https://github.com/steveklabnik/request_store#the-problem |
Gotcha. So i think i'm ok since all my def set_current_locale
request.env['app.current_locale'] = I18n.locale.to_s
end Since i just use the Thank you @tagliala ! |
I've had a deep dive into this (trying to get Devise and route_translator to play nice). The above solution works because it does set The other thing is that the example above is not resetting I18n.default_locale once the controller is done, thus "leaking" it up the middleware chain. This shouldn't be a threading issue, as default_locale turns out to be thread local (ie, not shared across threeads). However, it still isn't a great solution, as it will set the locale and default locale for the next request as well (up to the point where the before_action triggers), which means that the locale will actually differ within the Warden middleware depending on whether it has called out to the application or not. This might easily cause a lot of headache in the future. Improving @frahugo's solutionAs a side note, the code snippet given as solution above assigns a few local variables and such that are never actually used and could be shortened to: # hey there, don't copy-paste this, this isn't a real solution
def set_my_locale_from_url
return unless locale = params[RouteTranslator.locale_param_key] || RouteTranslator::Host.locale_from_host(request.host)
I18n.default_locale = I18n.locale = locale
end Also note that in my example I'm using main locales (en and de) for routes, but also support more specific locales for page content. To make it pick up the right URL helpers I have to set the default locale to the generic locale (ie de for de-CH/de-DE). This can be done with the following adjustment: # hey there, don't copy-paste this, this isn't a real solution
def set_my_locale_from_url
return unless locale = params[RouteTranslator.locale_param_key] || RouteTranslator::Host.locale_from_host(request.host)
I18n.locale = locale
I18n.default_locale = locale[/^[^\-]+/]
end Moving things to a MiddlewarePart of a proper solution is, in my opinion, to move the logic for setting the locale (and default locale) into a middleware: # lib/route_translator/middleware
class RouteTranslator::Middleware
def initialize(app)
@app = app
end
def call(env)
return @app.call(env) unless locale = parse_locale(env)
locale_save do
I18n.locale = locale
I18n.default_locale = locale[/^[^\-]+/]
@app.call(env)
end
end
private
def parse_locale(env)
request = ActionDispatch::Request.new(env)
RouteTranslator.locale_from_params(request.params) || RouteTranslator::Host.locale_from_host(request.host)
end
def locale_save
default_locale_was = I18n.default_locale
locale_was = I18n.locale
yield
ensure
I18n.default_locale = default_locale_was if default_locale_was
I18n.locale = locale_was if locale_was
end
end # config/initializers/i18n.rb
require 'route_translator/middleware'
Rails.application.config.middleware.insert_before(Warden::Manager, RouteTranslator::Middleware) Note that this differs slightly from the code I'm using, as I pick the specific locale from bothe the host and the Accept-Language header. Why does Devise use
|
Hi @rkh, thanks for the thorough explanation I do not recall how to reproduce this issue, is there by any chance the possibility to have a reduced test case with Rails 6.1, Devise, and Route Translator to reproduce it, so I can take a look? Maybe a refreshed version of https://github.com/jaimeiniesta/devisebug with a step-by-step guide to reproduce in the readme
The problem is that the proposed solution is to use The effects of having I can see that I cannot advise using
PR are welcomed |
I agree. before_filter is not the way to go. I'll see if I can find some time in the coming days to work on a PR/demo. |
Thanks, really appreciate Please attempt the simplest solution possible you mentioned about |
Here is an example: https://github.com/rkh/drt-example There's no easy fix while also maintaining a before action. I am considering to extract the middleware logic I'm using and put it in a separate gem, I think, that will address the issue, as I am not sure how much complexity you want to add to your library. |
Hi @rkh thanks for the reproducible test case. I can confirm the issues, and I confirm that I'm not sure that a middleware would be the best choice At the moment, Route Translator is opt-in, can we use the same approach with a middleware? Let's go back to the original issue here You should be able to translate the message by adding the following to # config/devise.rb
ActiveSupport.on_load(:devise_failure_app) do
def i18n_options(options)
locale_from_url = RouteTranslator.locale_from_params(params) || RouteTranslator::Host.locale_from_host(request.host)
options.merge(locale: locale_from_url)
end
end I think I can add a convenience method ( The second problem is the wrong redirect I think this could be fixed by monkey patching # config/devise.rb
ActiveSupport.on_load(:devise_failure_app) do
def route(scope)
:"new_#{scope}_session_#{route_translator_locale.to_s.underscore}_url"
end
def i18n_options(options)
options.merge(locale: route_translator_locale)
end
private
def route_translator_locale
RouteTranslator.locale_from_params(params) || RouteTranslator::Host.locale_from_host(request.host)
end
end If the above approach works, we can turn this into an extension or a module |
Makes sense. Middleware can also be opt-in. I'll be using this approach, as I can reuse the middleware for Sinatra applications. I've been copy-pasting I18n logic around a bit and want to move away from this, hence me considering putting the middleware into a separate library. Having separate domains to choose between German and English, and then picking a specific locale (like de-CH/de-DE) based on Accept-Language header is a common use case for me. I was also working on I18n itself a bit more and realised that setting I18n.default_locale (as the initial solution does) is not thread-safe at all and should not be done (as all I18n::Config instances share the same value via a class variable). I currently set up the middleware in the same initializer I use to configure RouteTranslator: I18n.backed = Localization::Backend.new
I18n.available_locales = %i[ en en-AU en-CA en-CY en-GB en-IE en-IN en-NZ en-US en-ZA de de-AT de-CH de-DE de-LI de-LU]
I18n.load_path += Dir[Rails.root.join('config/locales/**/*.{yml,rb}')]
RouteTranslator.config do |config|
config.locale_param_key = :main_locale
config.available_locales = [:en, :de]
# note: I've already realized that the line above should include all sub-locales, but haven't updated the app yet
if Rails.env.production?
config.host_locales = { … }
elsif File.readable?('/etc/hosts') and File.read('/etc/hosts') =~ /de\.local en\.local/
config.host_locales = { 'de.local' => :de, 'en.local' => :en }
else
config.force_locale = true
end
end
Rails.application.config.middleware.insert_before(Warden::Manager, Localization::Middleware) Here's the middleware I currently use in the specific app where I ran into the issue (and that fixes the issue for me): module Localization
class Middleware
def initialize(app) = @app = app
include Helpers
def call(env)
locale_was = I18n.locale
request = ActionDispatch::Request.new(env)
params = request.params
locale_from_url = RouteTranslator.locale_from_params(params) || RouteTranslator::Host.locale_from_host(request.host)
I18n.locale = locale_from_url if locale_from_url
I18n.locale = preferred_locale(request, I18n.locale, params[:locale])
@app.call(env)
ensure
I18n.locale = _locale_was
end
end
end It would also be possible to add a middleware automatically, but enable/disable it via # in route_translator/middleware.rb
class RouteTranslator::Middleware < I18n::Middleware
ActiveSupport.on_load(:after_initialize) do
# find the last-ish built-in middleware so we can insert our middleware before devise and other tools
last_middleware = Rails.application.middleware.reverse_each.detect { |m| m.name.start_with? 'ActionDispatch::' }
Rails.application.middleware.insert_after(last_middleware, self)
end
def call(env)
if RouteTranslator.config.set_locale_in_middeware
request = ActionDispatch::Request.new(env)
locale_from_url = RouteTranslator.locale_from_params(request.params) || RouteTranslator::Host.locale_from_host(request.host)
I18n.locale = locale_from_url if locale_from_url
end
super
end
end With such a middleware, all that would be needed to fix the Devise issue should be: RouteTranslator.config do |config|
config.set_locale_in_middeware = true
end But I do think just adding a small bit of Devise-specific logic should be fine, as it is fully backwards compatible. Instead of |
A general middleware functionality could be interesting to use route translator with other framework and with rails iself
Will it be possible to exclude some controllers with the middleware approach? An equivalent of
Thanks |
Any news about solving this translation-related issue the "normal" way? I can't translate "Invalid email or password." (related to issue heartcombo/devise#3647). In my
In my
Terminal output is:
|
I'm currently having an issue with the route_translator & Devise combo. When a user accesses a localized route, the gem properly sets the locale (in a
around_filter
method). If the user is not authenticated, Devise will issue a 401 and redirect to the sign-in page and also build a flash message. That logic is performed by a metal app called by the warden middleware.Since route_translator remembers the previous I18n.locale and default_locale, and sets them back after the yield, the Devise middleware ends up with the wrong I18n.locale when the requets completes.
I hacked my application controller to make it work (see below). I was wondering if the
around_filter
is really necessary in route_translator and if it could be replaced by abefore_filter
. I think the locale should not go back to the previous value and the middleware(s) running afterward should have access to that value.If you agree with the change, it will be a pleasure for me to submit a pull request. Thanks.
The text was updated successfully, but these errors were encountered: