AcceptLanguage

A lightweight, thread-safe Ruby library for parsing Accept-Language HTTP headers as defined in RFC 2616.

Version Yard documentation Ruby RuboCop License

Features

  • Thread-safe
  • No framework dependencies
  • Case-insensitive matching
  • BCP 47 language tag support
  • Wildcard and exclusion handling

Installation

gem "accept_language"

Usage

AcceptLanguage.parse("en-GB, en;q=0.9").match(:en, :"en-GB")
# => :"en-GB"

Quality values

Quality values (q-values) indicate preference order from 0 to 1:

parser = AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7")

parser.match(:en, :da)      # => :da
parser.match(:en, :"en-GB") # => :"en-GB"
parser.match(:fr)           # => nil

Language variants

A generic language tag matches its regional variants, but not the reverse:

AcceptLanguage.parse("fr").match(:"fr-CH")    # => :"fr-CH"
AcceptLanguage.parse("fr-CH").match(:fr)      # => nil

Wildcards and exclusions

The wildcard * matches any language. A q-value of 0 explicitly excludes a language:

AcceptLanguage.parse("de-DE, *;q=0.5").match(:fr)  # => :fr
AcceptLanguage.parse("*, en;q=0").match(:en)       # => nil
AcceptLanguage.parse("*, en;q=0").match(:fr)       # => :fr

Case sensitivity

Matching is case-insensitive but preserves the case of the available language tag:

AcceptLanguage.parse("en-GB").match("en-gb") # => "en-gb"
AcceptLanguage.parse("en-gb").match("en-GB") # => "en-GB"

Rails integration

# 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
    # HTTP_ACCEPT_LANGUAGE is the standardized key for the Accept-Language header in Rack/Rails
    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

Documentation

Versioning

This library follows Semantic Versioning 2.0.0.

License

Available as open source under the terms of the MIT License.