-
Notifications
You must be signed in to change notification settings - Fork 7
Rails Integration Guide
This guide shows you how to integrate AcceptLanguage with Ruby on Rails for automatic locale detection based on browser preferences.
Add the gem to your Gemfile:
gem "accept_language"Then run:
bundle installThe simplest approach is to add locale detection directly in your ApplicationController:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_locale
private
def set_locale
I18n.locale = preferred_locale || I18n.default_locale
end
def preferred_locale
header = request.headers["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*I18n.available_locales)
end
endThat's it! Rails will now automatically detect and use the user's preferred language.
In Rails, the Accept-Language header is available through:
# These are equivalent
request.headers["HTTP_ACCEPT_LANGUAGE"]
request.headers["Accept-Language"]
request.env["HTTP_ACCEPT_LANGUAGE"]The HTTP_ prefix is the Rack convention for HTTP headers.
Configure your available locales in config/application.rb:
# config/application.rb
module MyApp
class Application < Rails::Application
config.i18n.available_locales = [:en, :fr, :de, :ja]
config.i18n.default_locale = :en
end
endIf you support regional variants, include them:
config.i18n.available_locales = [:en, :"en-GB", :"en-US", :fr, :"fr-CA", :de]
config.i18n.default_locale = :enFor cleaner code, extract the logic into a concern:
# app/controllers/concerns/locale_detection.rb
module LocaleDetection
extend ActiveSupport::Concern
included do
before_action :set_locale
end
private
def set_locale
I18n.locale = detect_locale
end
def detect_locale
locale_from_params ||
locale_from_header ||
I18n.default_locale
end
def locale_from_params
locale = params[:locale]
return unless locale
return unless I18n.available_locales.include?(locale.to_sym)
locale.to_sym
end
def locale_from_header
header = request.headers["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*I18n.available_locales)
end
endThen include it in your controller:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include LocaleDetection
endA robust implementation checks multiple sources in order of priority:
# app/controllers/concerns/locale_detection.rb
module LocaleDetection
extend ActiveSupport::Concern
included do
before_action :set_locale
end
private
def set_locale
I18n.locale = detect_locale
end
def detect_locale
# Priority order:
# 1. URL parameter (?locale=fr)
# 2. User preference (if logged in)
# 3. Session storage
# 4. Cookie
# 5. Accept-Language header
# 6. Default locale
locale_from_params ||
locale_from_user ||
locale_from_session ||
locale_from_cookie ||
locale_from_header ||
I18n.default_locale
end
def locale_from_params
validate_locale(params[:locale])
end
def locale_from_user
return unless user_signed_in?
validate_locale(current_user.preferred_locale)
end
def locale_from_session
validate_locale(session[:locale])
end
def locale_from_cookie
validate_locale(cookies[:locale])
end
def locale_from_header
header = request.headers["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*I18n.available_locales)
end
def validate_locale(locale)
return unless locale
return unless I18n.available_locales.include?(locale.to_sym)
locale.to_sym
end
endWhen a user explicitly selects a language, save it:
# app/controllers/locales_controller.rb
class LocalesController < ApplicationController
def update
locale = params[:locale].to_sym
if I18n.available_locales.include?(locale)
# Save to session
session[:locale] = locale
# Save to cookie (persists across sessions)
cookies[:locale] = {
value: locale,
expires: 1.year.from_now
}
# Save to user record (if logged in)
current_user.update(preferred_locale: locale) if user_signed_in?
end
redirect_back fallback_location: root_path
end
endIf you use locale in URLs (e.g., /fr/articles), combine it with AcceptLanguage for the default:
# config/routes.rb
Rails.application.routes.draw do
scope "/:locale", locale: /en|fr|de/ do
resources :articles
# ... other routes
end
# Redirect root to localized root
root to: "redirects#locale_root"
end# app/controllers/redirects_controller.rb
class RedirectsController < ApplicationController
def locale_root
locale = detect_locale
redirect_to "/#{locale}"
end
private
def detect_locale
header = request.headers["HTTP_ACCEPT_LANGUAGE"]
return I18n.default_locale if header.nil?
AcceptLanguage.parse(header).match(*I18n.available_locales) || I18n.default_locale
end
endConfigure Rails to handle missing translations gracefully:
# config/application.rb
config.i18n.fallbacks = trueOr set up a custom fallback chain:
# config/initializers/locale.rb
Rails.application.config.after_initialize do
I18n.fallbacks = {
"en-GB": [:en],
"en-US": [:en],
"fr-CA": [:fr],
"de-AT": [:de],
"de-CH": [:de]
}
endFor API-only applications, the pattern is similar:
# app/controllers/api/base_controller.rb
module Api
class BaseController < ActionController::API
before_action :set_locale
private
def set_locale
I18n.locale = preferred_locale || I18n.default_locale
end
def preferred_locale
header = request.headers["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*I18n.available_locales)
end
end
endIf you use page or fragment caching, include the locale in cache keys:
# In views
<% cache [current_locale, @article] do %>
<%= render @article %>
<% end %># Helper method
def current_locale
I18n.locale
endFor HTTP caching, set the Vary header:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_vary_header
private
def set_vary_header
response.headers["Vary"] = "Accept-Language"
end
endTest your locale detection:
# spec/requests/locale_detection_spec.rb
require "rails_helper"
RSpec.describe "Locale detection" do
describe "Accept-Language header" do
it "detects French" do
get root_path, headers: { "HTTP_ACCEPT_LANGUAGE" => "fr, en;q=0.8" }
expect(I18n.locale).to eq(:fr)
end
it "falls back to default for unsupported language" do
get root_path, headers: { "HTTP_ACCEPT_LANGUAGE" => "ja" }
expect(I18n.locale).to eq(I18n.default_locale)
end
it "handles missing header" do
get root_path
expect(I18n.locale).to eq(I18n.default_locale)
end
it "handles complex preferences" do
get root_path, headers: { "HTTP_ACCEPT_LANGUAGE" => "de-CH, de;q=0.9, fr;q=0.8" }
expect(I18n.locale).to eq(:de)
end
end
endHere's a complete, production-ready implementation:
# config/application.rb
config.i18n.available_locales = [:en, :fr, :de, :ja]
config.i18n.default_locale = :en
config.i18n.fallbacks = true# app/controllers/concerns/locale_detection.rb
module LocaleDetection
extend ActiveSupport::Concern
included do
before_action :set_locale
before_action :set_vary_header
end
private
def set_locale
I18n.locale = detect_locale
session[:locale] = I18n.locale
end
def detect_locale
locale_from_params ||
locale_from_session ||
locale_from_header ||
I18n.default_locale
end
def locale_from_params
locale = params[:locale]
return unless locale && valid_locale?(locale)
locale.to_sym
end
def locale_from_session
locale = session[:locale]
return unless locale && valid_locale?(locale)
locale.to_sym
end
def locale_from_header
header = request.headers["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*I18n.available_locales)
end
def valid_locale?(locale)
I18n.available_locales.include?(locale.to_sym)
end
def set_vary_header
response.headers["Vary"] = "Accept-Language"
end
end# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include LocaleDetection
end- Rack Integration Guide — For non-Rails Rack applications
- Real-World Examples — More patterns and use cases
- Understanding Quality Values — Deep dive into preference handling