route_translator: I18n.locale not available to middlewares when request completes

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 a before_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.

class ApplicationController < ActionController::Base
  # HACK for route_translators
  skip_around_filter :set_locale_from_url
  before_filter :set_my_locale_from_url

  def set_my_locale_from_url
    tmp_default_locale = RouteTranslator::Host.locale_from_host(request.host)
    if tmp_default_locale
      current_default_locale = I18n.default_locale
      I18n.default_locale    = tmp_default_locale
    end

    tmp_locale = params[RouteTranslator.locale_param_key] || tmp_default_locale
    if tmp_locale
      current_locale = I18n.locale
      I18n.locale    = tmp_locale
    end
  end
end

About this issue

  • Original URL
  • State: open
  • Created 9 years ago
  • Reactions: 1
  • Comments: 27 (1 by maintainers)

Most upvoted comments

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 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.

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 I18n.default_locale in addition to I18n.locale, which is what will be picked up by Devise when deciding which URL helper method to call. If it is left to :en (the default), it will always call new_user_session_en_url', which work somewhat ok if you do path or param based language detection, but break for host based links.

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 solution

As 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 Middleware

Part 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 I18n.default_locale?

At first I just moved the locale logic into a middleware, without also setting default_locale (which leads to broken redirects), until I saw the above solution. Which got me wondering why Devise seems to use I18n.default_locale. It doesn’t. Devise ends up calling something like new_user_session_url(this is happening in lib/devise/failure_app.rb if anyone is following along), which gives control back to route translator, which in turn is falling back to default_locale.

This could be avoided/improved if route_name_for would first try I18n.fallbacks instead of going straight to I18n.default_locale.