Language negotiation with Ruby

The web is made up of hyperlinks to connect pages to each other, as well as natural languages to bring human beings together. And just like hyperlinks, no one can count all human languages in their entirety. However, codes exist to help identify them. At least that's the goal of the BCP 47 specification, which provides tags for the identification of language according to their name, region and writing system.

In web services, client preferences can be mentioned within the Accept-Language HTTP header field.

For example, a native Japanese speaker may wish to select the Japanese language as the first preferred language in his browser settings, so that it is specified in his requests:

Accept-Language: ja

Let's adapt this parameter a little to express a preference for the dialect of the Kansai region, this user being from Osaka.

Accept-Language: ja-KS, ja;q=0.9

Now imagine that this user also has some notions of Thai, but that he can only read it with the Latin alphabet:

Accept-Language: ja-KS, ja;q=0.9, th-Latn;q=0.8

Finally, let us add that this user is ready to fall back on another language if necessary, with the exception of French and English:

Accept-Language: ja-KS, ja;q=0.9, th-Latn;q=0.8, *;q=0.1 fr;q=0, en;q=0

In Ruby, there is a small library that can facilitate language negotiations between client and server: accept_language.

Considering a server with Japanese and Korean translations of a particular resource, we could find the intersection of the languages the user prefers and the languages the application supports like so:

AcceptLanguage.parse("ja-KS, ja;q=0.9, th-Latn;q=0.8, *;q=0.1 fr;q=0, en;q=0").match(:ko, :ja) # => :ja

What in a Rails app might look like this:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :best_locale_from_request!

  def best_locale_from_request!
    I18n.locale = best_locale_from_request
  end

  def best_locale_from_request
    return I18n.default_locale unless request.headers.key?("HTTP_ACCEPT_LANGUAGE")

    string = request.headers.fetch("HTTP_ACCEPT_LANGUAGE")
    locale = AcceptLanguage.parse(string).match(*I18n.available_locales)

    # If the server cannot serve any matching language,
    # it can theoretically send back a 406 (Not Acceptable) error code.
    # But, for a better user experience, this is rarely done and more
    # common way is to ignore the Accept-Language header in this case.
    return I18n.default_locale if locale.nil?

    locale
  end
end

Have fun matching up with as many languages as possible, dear friends!

22