A long-lived project that still receives updates
A lightweight, thread-safe Ruby library for parsing the Accept-Language HTTP header as defined in RFC 2616, with full support for BCP 47 language tags.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

AcceptLanguage

A lightweight, thread-safe Ruby library for parsing the Accept-Language HTTP header field.

This implementation conforms to:

Note

RFC 7231 obsoletes RFC 2616 (the original HTTP/1.1 specification). The Accept-Language header behavior defined in RFC 2616 Section 14.4 remains unchanged in RFC 7231, ensuring full backward compatibility.

Version Yard documentation Ruby RuboCop License

Installation

gem "accept_language"

Usage

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

Behavior

Quality values

Quality values (q-values) indicate relative preference, ranging from 0 (not acceptable) to 1 (most preferred). When omitted, the default is 1.

Per RFC 7231 Section 5.3.1, valid q-values have at most three decimal places: 0, 0.7, 0.85, 1.000. Invalid q-values cause the associated language range to be ignored.

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

parser.match(:en, :da)      # => :da       (q=1 beats q=0.8)
parser.match(:en, :"en-GB") # => :"en-GB"  (q=0.8 beats q=0.7)
parser.match(:ja)           # => nil       (no match)

Declaration order

When multiple languages share the same q-value, declaration order in the header determines priority—the first declared language wins:

AcceptLanguage.parse("en;q=0.8, fr;q=0.8").match(:en, :fr)
# => :en  (declared first)

AcceptLanguage.parse("fr;q=0.8, en;q=0.8").match(:en, :fr)
# => :fr  (declared first)

Basic Filtering

This library implements the Basic Filtering matching scheme defined in RFC 4647 Section 3.3.1. A language range matches a language tag if, in a case-insensitive comparison, it exactly equals the tag, or if it exactly equals a prefix of the tag such that the first character following the prefix is -.

AcceptLanguage.parse("de-de").match(:"de-DE-1996")
# => :"de-DE-1996"  (prefix match)

AcceptLanguage.parse("de-de").match(:"de-Deva")
# => nil  ("de-de" is not a prefix of "de-Deva")

AcceptLanguage.parse("de-de").match(:"de-Latn-DE")
# => nil  ("de-de" is not a prefix of "de-Latn-DE")

Prefix matching respects hyphen boundaries:

AcceptLanguage.parse("zh").match(:"zh-TW")
# => :"zh-TW"  ("zh" matches "zh-TW")

AcceptLanguage.parse("zh").match(:zhx)
# => nil  ("zh" does not match "zhx" — different language code)

AcceptLanguage.parse("zh-TW").match(:zh)
# => nil  (more specific range does not match less specific tag)

Wildcards

The wildcard * matches any language not matched by another range in the header. This behavior is specific to HTTP, as noted in RFC 4647 Section 3.3.1.

AcceptLanguage.parse("de, *;q=0.5").match(:ja)
# => :ja  (matched by wildcard)

AcceptLanguage.parse("de, *;q=0.5").match(:de, :ja)
# => :de  (explicit match takes precedence)

Exclusions

A q-value of 0 explicitly marks a language as not acceptable:

AcceptLanguage.parse("*, en;q=0").match(:en)
# => nil  (English explicitly excluded)

AcceptLanguage.parse("*, en;q=0").match(:ja)
# => :ja  (Japanese matched by wildcard)

Exclusions apply via prefix matching:

AcceptLanguage.parse("*, en;q=0").match(:"en-GB")
# => nil  (en-GB excluded via "en" prefix)

Case insensitivity

Matching is case-insensitive per RFC 4647 Section 2, but the original case of available language tags is preserved in the return value:

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

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

BCP 47 language tags

Full support for BCP 47 language tags including script subtags, region subtags, and variant subtags:

# Script subtags
AcceptLanguage.parse("zh-Hant").match(:"zh-Hant-TW", :"zh-Hans-CN")
# => :"zh-Hant-TW"

# Variant subtags
AcceptLanguage.parse("de-1996, de;q=0.9").match(:"de-CH-1996", :"de-CH")
# => :"de-CH-1996"

Integration examples

Rack

# config.ru
class LocaleMiddleware
  def initialize(app, available_locales:, default_locale:)
    @app = app
    @available_locales = available_locales
    @default_locale = default_locale
  end

  def call(env)
    locale = detect_locale(env) || @default_locale
    env["rack.locale"] = locale
    @app.call(env)
  end

  private

  def detect_locale(env)
    header = env["HTTP_ACCEPT_LANGUAGE"]
    return unless header

    AcceptLanguage.parse(header).match(*@available_locales)
  end
end

Ruby on Rails

# 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

Standards compliance

Supported specifications

Specification Description Status
RFC 7231 §5.3.5 Accept-Language header field ✅ Supported
RFC 7231 §5.3.1 Quality values (qvalues) ✅ Supported
RFC 4647 §2.1 Basic Language Range syntax ✅ Supported
RFC 4647 §3.3.1 Basic Filtering scheme ✅ Supported
BCP 47 Language tag structure ✅ Supported

Not implemented

Specification Description Reason
RFC 4647 §2.2 Extended Language Range Not used by HTTP
RFC 4647 §3.3.2 Extended Filtering Not used by HTTP
RFC 4647 §3.4 Lookup scheme Design choice — Basic Filtering is appropriate for HTTP content negotiation

Documentation

Versioning

This library follows Semantic Versioning 2.0.

License

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