Web applications often need to cater to users from around the world. One of the ways they can provide a better user experience is by presenting the content in the user's preferred language. This is where the Accept-Language
HTTP header comes into play. Sent by the client (usually a web browser), this header tells the server the list of languages the user understands, and the user's preference order.
Parsing the Accept-Language
header can be complex due to its flexible format defined in RFC 2616. For instance, it can specify languages, countries, and scripts with varying degrees of preference (quality values).
Accept Language
is a lightweight, thread-safe Ruby library designed to parse the Accept-Language
header, making it easier for your application to determine the best language to respond with. It calculates the intersection of the languages the user prefers and the languages your application supports, handling all the complexity of quality values and wildcards.
Whether you're building a multilingual web application or just trying to make your service more accessible to users worldwide, Accept Language
offers a reliable, simple solution.
There are a myriad of tools out there, so why should you consider Accept Language for your next project? Here's why:
- Thread-Safe: Multithreading can present unique challenges when dealing with shared resources. Our implementation is designed to be thread-safe, preventing potential concurrency issues.
- Compact and Robust: Despite being small in size, Accept Language can handle even the trickiest cases with grace, ensuring you have a reliable tool at your disposal.
- Case-Insensitive Matching: In line with the principle of robustness, our tool matches both strings and symbols regardless of case, providing greater flexibility.
- Independent of Framework: While many tools require Rails, Rack, or i18n to function, Accept Language stands on its own. It works perfectly well without these dependencies, increasing its adaptability.
- BCP 47 Support: BCP 47 defines a standard for language tags. This is crucial for specifying languages unambiguously. Accept Language complies with this standard, ensuring accurate language identification.
Add this line to your application's Gemfile:
gem "accept_language"
And then execute:
bundle install
Or install it yourself as:
gem install accept_language
The Accept Language
library helps web applications serve content in the user's preferred language by parsing the Accept-Language
HTTP header. This header indicates the user's language preferences and their priority order.
The library has two main methods:
AcceptLanguage.parse(header)
: Parses the Accept-Language headermatch(*available_languages)
: Matches against the languages your application supports
AcceptLanguage.parse("fr-CH, fr;q=0.9").match(:fr, :"fr-CH") # => :"fr-CH"
# Header: "da" (Danish is the preferred language)
# Available: :en and :da
AcceptLanguage.parse("da").match(:en, :da) # => :da
# No match available - returns nil
AcceptLanguage.parse("da").match(:fr, :en) # => nil
Q-values range from 0 to 1 and indicate preference order:
# Header: "da, en-GB;q=0.8, en;q=0.7"
# Means:
# - Danish (da): q=1.0 (highest priority)
# - British English (en-GB): q=0.8 (second choice)
# - Generic English (en): q=0.7 (third choice)
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da) # => :da
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :"en-GB") # => :"en-GB"
The library handles specific language variants (regional or script variations):
# Specific variants must match exactly
AcceptLanguage.parse("fr-CH").match(:fr) # => nil (Swiss French ≠ Generic French)
# But generic variants can match specific ones
AcceptLanguage.parse("fr").match(:"fr-CH") # => :"fr-CH"
# Script variants are also supported
AcceptLanguage.parse("uz-Latn-UZ").match("uz-Latn-UZ") # => "uz-Latn-UZ"
The *
wildcard matches any language, and q=0
excludes languages:
# Accept any language but prefer German
AcceptLanguage.parse("de-DE, *;q=0.5").match(:fr) # => :fr (matched by wildcard)
# Accept any language EXCEPT English
AcceptLanguage.parse("*, en;q=0").match(:en) # => nil (explicitly excluded)
AcceptLanguage.parse("*, en;q=0").match(:fr) # => :fr (matched by wildcard)
# Header: "de-LU, fr;q=0.9, en;q=0.7, *;q=0.5"
# Means:
# - Luxembourg German: q=1.0 (highest priority)
# - French: q=0.9 (second choice)
# - English: q=0.7 (third choice)
# - Any other language: q=0.5 (lowest priority)
header = "de-LU, fr;q=0.9, en;q=0.7, *;q=0.5"
parser = AcceptLanguage.parse(header)
parser.match(:de, :"de-LU") # => :"de-LU" (exact match)
parser.match(:fr, :en) # => :fr (higher q-value)
parser.match(:es, :it) # => :es (matched by wildcard)
The matching is case-insensitive but preserves the case of the returned value:
AcceptLanguage.parse("en-GB").match("en-gb") # => "en-gb"
AcceptLanguage.parse("en-gb").match("en-GB") # => "en-GB"
# 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
AcceptLanguage uses Semantic Versioning 2.0.0
The gem is available as open source under the terms of the MIT License.