Say I want to localize my app for English and Japanese visitors. How
do I know which language to use for each HTTP request? Here''s a way
to negotiate the best choice.
What you do with the negotiated language is up to you: use it to
choose a message dictionary, to decide how to format numbers and
dates, to set preferred units of measurement, etc. Drop this into
lib/negotiate_request_language.rb and follow your nose.
# Mix in to your controller to transparently negotiate a language to
# each request.
#
# require ''negotiate_request_language''
#
# class ApplicationController < ActionController::Base
# # Figure out which of these languages the visitor wants.
# # Use the lang method to retrieve the matching language.
# accepts_languages ''en'', ''ja''
#
# def index
# render_text greeting
# end
#
# protected
# def greeting
# case lang
# when ''en'': ''and a good day to you
sir''
# when ''ja'': ''konnichiwa''
# else '':-)''
# end
# end
# end
#
# Further reading on language negotiation:
# http://www.cs.tut.fi/~jkorpela/multi/
# http://ppewww.ph.gla.ac.uk/~flavell/www/lang-neg.html
module ActionController
class Base
def self.accepts_languages(*langs)
include NegotiateRequestLanguage
self.accepted_languages = langs.flatten
end
end
end
module NegotiateRequestLanguage
# Look for language in this order. Expects a corresponding
# accept_lang_from_#{method} method.
ACCEPT_LANG_METHODS = [
:params,
:cookie,
:header,
:environment,
:failsafe
]
def self.append_features(base)
super
base.cattr_accessor :accepted_languages
base.send :before_filter, :negotiate_language
base.helper_method :lang
end
protected
attr_reader :lang
# Check whether we accept a language.
def accepts_lang?(lang)
accepted_languages.include? lang
end
# Negotiate a language to the HTTP request. Search for a request
# parameter, a cookie, the Accept-Language HTTP header, and the
LANG
# environment variable. Fall back to ''en''.
def negotiate_language
ACCEPT_LANG_METHODS.find { |m| @lang = send("accept_lang_from_#
{m}") }
end
private
# Look for lang in query params (could be from a custom route.)
If found,
# set a long-lived cookie to pin the match.
def accept_lang_from_params
if lang = params[:lang].downcase and accepts_lang?(lang)
cookies[''lang''] = { :value => lang, :expires =>
10.years.from_now }
lang
end
end
# If lang cookie is present, check whether we have a match.
# If the language is not accepted, clear the cookie.
def accept_lang_from_cookie
if lang = cookies[''lang''].downcase
if accepts_lang?(lang)
lang
else
cookies[''lang''] = nil
end
end
end
# Look for languages in the Accept-Language HTTP header.
def accept_lang_from_header
if header = request.env[''HTTP_ACCEPT_LANGUAGE'']
accept = header.split('','')
lang = accept.shift.to_s.strip.downcase
# Take the preferred language if accepted.
if accepts_lang?(lang)
lang
# Otherwise, parse the quality string and pick the best.
else
best = accept.inject([nil, 0.0]) do |best, pref|
lang, quality = pref.split('';'')
lang = lang.strip.downcase
if md = /q=(.+)/.match(quality)
quality = md.captures.first.to_f
else
quality = 1.0
end
if (best.nil? or quality > best[1]) and accepts_lang?(lang)
best = [lang, quality]
end
end
if best and accepts_lang?(best[0])
best[0]
end
end
end
end
# Look in the environment for a default language if all else fails.
def accept_lang_from_environment
ENV[''LANG''] if
accepts_lang?(ENV[''LANG''])
end
# And go to ''en'' if the environment fails too.
def accept_lang_from_failsafe
''en''
end
end
I extracted this from code that uses Locale objects instead of
language strings, so beware bugs. I have test cases that could be
adapted also if you''d like to extend it further.
Best,
jeremy