There occurs an interesting problem when you are using Ruby’s GetText to translate a site that requires authentication.
Most likely, you have a before_filter that calls a method that authenticates the user for certain methods. Within this filter you probably have something like:
user = (attempt_oauth or
attempt_basic_auth or
attempt_session_auth or
attempt_cookie_auth or
(@authentication_attempt = nil))
If the user has clicked “Remember Me” and stored an cookie locally, than we’re going to validate the user using attempt_cookie_auth. But what if the user has a default language set (stored most likely in the database). We’ll assume there is a method user.lang that returns the default language or nil.
Your first attempt at setting this here may be this one.
NOTE: THIS IS WRONG!
# If the user has a default lang, set it here.
if u && u.lang && (LANGUAGE_CODES.include?(u.lang))
cookies[:lang] = u.lang unless cookies[:lang]
end
That sets the cookie for the user, but the GetText stack has already been invoked, so the language will be that sent over by the browser. Refreshing the page will cause GetText to pick up the cookie value and render the proper language.
Next, we try using set_locale (GetText.set_locale) to set the language manually, which seems like a perfectly reasonable option.
NOTE: THIS IS WRONG!
# If the user has a default lang, set it here.
if u && u.lang && (LANGUAGE_CODES.include?(u.lang))
cookies[:lang] = u.lang unless cookies[:lang]
set_locale u.lang
end
Why is this bad? The set_locale method persists for the life of the Ruby instance (in this case, Mongrel), not the session. This means that there will be a literal battle for contention over which language to use.
User A sets the language to JA and the page renders JA.
User B sets the language to EN and the page renders JA.
User A sets the language to JA and the page renders EN.
You see the problem! We want a clean slate at the beginning of each request so GetText has no pre-conceived notions about what the language should be. Hrm … let’s try something.
NOTE: THIS IS WRONG!
# If the user has a default lang, set it here.
if u && u.lang && (LANGUAGE_CODES.include?(u.lang))
cookies[:lang] = u.lang unless cookies[:lang]
set_locale u.lang
else
set_locale nil
end
Note … to be thorough, and because the authentication stack is not called for all methods, we also add this in our application.rb
before_init_gettext :set_default_locale
def set_default_locale; set_locale nil; end
Here, everything seems to work!
We can even write a test to verify that the contention above doesn’t occur.
def test_lang_should_be_set_on_a_per_session_basis
bob.lang = 'ja'
assert bob.save
bob.reload
assert_equal 'ja', bob.lang
post '/sessions', {:username_or_email => bob.screen_name, :password => 'foo'}
assert_response :redirect
follow_redirect!
assert_response :redirect
follow_redirect!
assert_response :success
assert_equal 'ja', Locale.current.language
phoenix.reload
assert_equal nil, phoenix.lang
post '/sessions', {:username_or_email => phoenix.screen_name, :password => 'foo'}
assert_response :redirect
follow_redirect!
assert_response :redirect
follow_redirect!
assert_response :success
assert_equal 'en', Locale.current.language
end
But alas, download the Japanese version of Firefox and visit the page.

The browser is hungry for UTF-8 data. As you can see, the part of the page in which we set the locale manually using set_locale is being pushed as SHIFT-JIS. The blue highlighted area is actually outside of the application.rb controller stack, so is sent via GetText’s default assumptions: UTF-8.
But this isn’t a problem in Safari or Internet Explorer. Why? Let’s look at the value of HTTP_ACCEPT_LANGUAGE.
Firefox : "HTTP_ACCEPT_CHARSET" => "EUC-KR,utf-8;q=0.7,*;q=0.7"
Safari : "HTTP_ACCEPT_CHARSET" => ???
Safari actually doesn’t pass one, while Firefox gives precedence EUC-KR (effectively SHIFT-JIS) over UTF-8.
Sigh. Things are looking grim. Back to the drawing board. Let’s look at the order of precedence for how the Rails GetText integration determines the langauge.
The language passed to GetText.bindtextdomain.
The lang query param. ( url?lang=foo )
The lang cookie.
The value of HTTP_ACCEPT_LANGUAGE passed by the browser.
The default (English).
Aha! GetText.bindtextdomain! Looking at the RDocs and source, this is not only called within the init_gettext method, but can be set on a per session basis without mucking with default language settings!
Let’s try!
NOTE: THIS IS CORRECT! CELEBRATE!
# If the user has a default lang, set it here.
if u && u.lang && (LANGUAGE_CODES.include?(u.lang))
cookies[:lang] = u.lang unless cookies[:lang]
GetText.bindtextdomain("Twitter", :locale => u.lang)
end
So finally, we can set the language on a request manually after the GetText stack has already been invoked. Whew!