Skip to content

Rails Integration Guide

Cyril Kato edited this page Jan 20, 2026 · 1 revision

Table of Contents

Rails Integration Guide

This guide shows you how to integrate AcceptLanguage with Ruby on Rails for automatic locale detection based on browser preferences.

Basic Setup

Add the gem to your Gemfile:

gem "accept_language"

Then run:

bundle install

Quick Implementation

The 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
end

That's it! Rails will now automatically detect and use the user's preferred language.

Understanding the Header Access

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.

Configuration

Setting Available Locales

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
end

With Regional Variants

If you support regional variants, include them:

config.i18n.available_locales = [:en, :"en-GB", :"en-US", :fr, :"fr-CA", :de]
config.i18n.default_locale = :en

Using a Concern

For 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
end

Then include it in your controller:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include LocaleDetection
end

Priority Chain

A 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
end

Persisting User Choice

When 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
end

URL-Based Locales

If 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
end

Handling Missing Translations

Configure Rails to handle missing translations gracefully:

# config/application.rb
config.i18n.fallbacks = true

Or 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]
  }
end

API Controllers

For 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
end

Caching Considerations

If 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
end

For 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
end

Testing

Test 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
end

Complete Example

Here'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

What's Next?

Clone this wiki locally