19

I just realized that the recommended Rails way to set locale in your controller

before_filter :set_locale

def set_locale
  I18n.locale = params[:locale] || I18n.default_locale
end

sets the locale globally. The code above works, but I wonder is default_locale really default if you have to type it explicitly?

What I'd expect is to have a locale per request (like we have session per request) and doing something like:

def set_locale
  locale = params[:locale] if params[:locale]
end

And having I18n.default_locale used by default otherwise. This would match ideally the optional locale in path:

# config/routes.rb
scope "(:locale)", :locale => /en|nl/ do
  resources :books
end

For now if for some reason I skip locale setting in some action it uses the locale set in the previous request which could be from another user!

And isn't there a potential race condition as one request can change global I18n.locale while another request (having set another locale beforehande) is in the middle of rendering?


UPDATE: Some details I found for now, from the I18n documentstion:

Sets the current locale pseudo-globally, i.e. in the Thread.current hash def locale=(locale)

Now I want to understand if every request is a separate thread.


UPDATE 2: See my answer for explanation.

khustochka
  • 2,346
  • 20
  • 29

2 Answers2

13

So now the final answer. TL;DR Setting locale acts as global only when you use threaded web servers, like Thin and Puma.

As I mentioned, I18n.locale=

Sets the current locale pseudo-globally, i.e. in the Thread.current hash

So it is supposed to be per-request, and it works this way in Webrick and Unicorn.

But if you use threaded web server like Thin or Puma, seems that the thread lives longer, and the value is preserved for future requests, until it is changed explicitly. Where I learned it is from the new Steve Klabnik's gem request_store:

If you need global state, you've probably reached for Thread.current.

<...>

So people are using those fancy threaded web servers, like Thin or Puma. But if you use Thread.current, and you use one of those servers, watch out! Values can stick around longer than you'd expect, and this can cause bugs.

Community
  • 1
  • 1
khustochka
  • 2,346
  • 20
  • 29
  • I'm having the exact same problem, but i'm using Unicorn! Any idea how to solve this? thanks – Uri Klar Jul 24 '14 at 06:12
  • I think when you say "I18n.set_locale", you actually mean "I18n.locale=" right? http://www.rubydoc.info/github/svenfuchs/i18n/I18n/Config:locale= – lulalala Nov 11 '14 at 02:16
  • You can face the same problem in non threaded web servers. If you have different controllers with different purposes (aka, public controller and api controller) and one of them is not setting properly the locale you will get the locale from the previous request. x_X' – Ferran Basora Oct 12 '16 at 10:02
3

Recommended code from above does not set locale globally it sets it by request.

before_filter :set_locale

def set_locale
  I18n.locale = params[:locale] || I18n.default_locale
end

Code is usually place in BaseController so before each page is render it is triggered and set. There is no race conditions since every page will trigger this code and I18n locale will be calculated there. You can expand this to let's say looks for users locale, than session locale, than request params, than uses English.

def set_locale
  I18n.locale = @user.locale || session[:locale] || params[:locale] || :en
end

In other words if you set local on one page let's say in home controller to german and got to dashboard controller you will see default language (english). Since change is not global. That is why code is placed in base controller. Hope it makes sense.

Haris Krajina
  • 14,824
  • 12
  • 64
  • 81
  • 2
    `I18n` is a global module, and `I18n.locale=` changes its state. I agree that the recommended way of explicitly setting locale for every request via `before_filter` works as expected in general. Still I wonder if Rails requests can be ran simultaneously in different threads - if yes, I see it possible that request 1 sets `I18n.locale`, then request 2 set different `I18n.locale`, then request 1 starts rendering using the new (incorrect) locale. Sorry if I'm getting your answer wrong. – khustochka Feb 14 '12 at 08:50
  • @khustochka That would happen if the same thread stopped processing request 1 for some reason, and then started processing request 2 and changed the locale, then came back to request 1 and continue processing it with the new locale. As global means per thread. – Brary May 09 '18 at 07:32