Parsing Accept-Language in Rails

Recently, I had to know which language is preferred by someone visiting the web site I was developing for. Hopefully, HTTP is well designed enough that you should not have to ask your visitors for this preference but can elegantly guess what it is by analyzing the Accept-Language HTTP request header.
This header is seamlessly sent to the HTTP server by your browser. It's clearly defined by the HTTP 1.1 specification if you want all the details. Briefly, the value is a "set of natural languages that are preferred as a response to the request.". Each language "may be given an associated quality value which represents an estimate of the user's preference for the languages specified by that range. The quality value defaults to 1."
To leverage this header, I simply wrote a "helper" method that I made available to all the controllers. I named this method accepted_languages and put it on the ApplicationController class (RAILS_ROOT/app/models/application.rb). It takes no parameter and returns an array where each element is a pair [language tag, quality], sorted by ascending quality.
Here's the code

class ApplicationController < ActionController::Base
  
  # ...
  
  ##
  # Returns the languages accepted by the visitors, sorted by quality
  # (order of preference).
  ##
  def accepted_languages()
    # no language accepted
    return [] if request.env["HTTP_ACCEPT_LANGUAGE"].nil?
    
    # parse Accept-Language
    accepted = request.env["HTTP_ACCEPT_LANGUAGE"].split(",")
    accepted = accepted.map { |l| l.strip.split(";") }
    accepted = accepted.map { |l|
      if (l.size == 2)
        # quality present
        [ l[0].split("-")[0].downcase, l[1].sub(/^q=/, "").to_f ]
      else
        # no quality specified =&gt; quality == 1
        [ l[0].split("-")[0].downcase, 1.0 ]
      end
    }
    
    # sort by quality
    accepted.sort { |l1, l2| l1[1] <=> l2[1] }
  end
end
Look at how you get the request header inside Rails. It's available from the environment attached to the request through a Ruby home-made key (HTTP_ACCEPT_LANGUAGE). This is due to how the HTTP headers are transmitted between the Rails framework and the HTTP server that stands upfront. This is directly derived from how the CGI standard Ruby library does for passing an HTTP request from a web server to a standalone program, here your Rails application.
Most of the time, your controllers only want to retain one language, the one presumably preferred by the visitor. Additionally, your application only supports a few languages. This is the context I had to fit into and I found useful to add a second method to the ApplicationController class.
This method, named preferred_language, takes 2 optional parameters

  1. an array that contains the list of language( tag)s supported by your application; it is used to filter out the set of languages accepted by the visitor.
  2. a default language (tag) that is returned to the caller when the browser does not expose any accepted languages or not a single language that is supported by the application; this second parameter represnts the default language of your application.
By default, the application supports any language but has a preference for english.
class ApplicationController < ActionController::Base
  
  # ...
  
  ##
  # Returns the language preferred by the user.
  ##
  def preferred_language(supported_languages=[], default_language="en")
    
    # only keep supported languages
    preferred_languages = accepted_languages.select {|l|
     (supported_languages || []).include?(l[0]) }
    
    if preferred_languages.empty?
      # the browser does accept any supported languages
      # => default to english
      default_language
    else
      # take the highest quality among accepted (and thus supported) languages
      preferred_languages.last[0]
    end
  end
end
If you want to reuse this piece of code (what it's meant for), please remember that it does not take care of country codes. As explained in the HTTP spec, each language tag starts with an ISO-639 language abbreviation and may be followed by an ISO-3166 country code. The latter is simply ignored by this code.
Finally, the parsing and exposure of all the HTTP headers are worth writing a Rails plug-in or a gem. I might do it one day unless someone points me at a similar contribution.


UPDATE: This morning, I found the HTTP Accept-Language plugin that does exactly what I describe hereby, quite similarly. The methods used for accessing the preferred language(s) are bound to the request object, which I admit is better. I'm quite sure this plugin did not exist when I initiated this development (some months ago).

UPDATE (2013/05/10): Naomi Kyoto suggested a much more concise and elegant code snippet for the accepted_languages method. I recommend to use her gist. Thanks for sharing !



6 comments

  1. Naomi Kyoto  

    May 9, 2013 at 9:04 AM

    This comment has been removed by the author.
  2. Naomi Kyoto  

    May 9, 2013 at 9:06 AM

    This comment has been removed by the author.
  3. Naomi Kyoto  

    May 9, 2013 at 9:13 AM

    I would leave a highly superior solution to this problem but pasting code on your blog is a nightmare. I forfeit.

    here's a gist instead

  4. Laurent Farcy  

    May 10, 2013 at 9:27 AM

    Thanks for your comment Naomi. I agree your code is much more concise and elegant. And it does the job !

    I'd recommend everyone to go with your solution. And I'm about to make a second update to the post to suggest so.

    I can read you're not in favor of gems. Otherwise, I'd have suggested to initiate a new one on GitHub.

    Finally, it's great to see that a post written in 2008 can still get readers and, most importantly, constructive feedbacks.

  5. j michel  

    May 23, 2014 at 12:04 PM

    Great post and a good stuff for me to takeaway

  6. Bobb  

    May 23, 2014 at 1:19 PM

    Very Nice post!!thanks for sharing..

Recent Entries

Recent Comments

Ruby and Rails> Recommended