-
Notifications
You must be signed in to change notification settings - Fork 7
Rack Integration Guide
This guide shows you how to integrate AcceptLanguage with Rack-based applications, including Sinatra, Hanami, Roda, and pure Rack apps.
Rack is the interface between Ruby web servers and frameworks. The Accept-Language header is available in the Rack environment as HTTP_ACCEPT_LANGUAGE.
# In any Rack app
env["HTTP_ACCEPT_LANGUAGE"]
# => "fr-CH, fr;q=0.9, en;q=0.8"The cleanest way to handle locale detection is with middleware. This works with any Rack-compatible framework.
# lib/locale_middleware.rb
require "accept_language"
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 if header.nil?
AcceptLanguage.parse(header).match(*@available_locales)
end
end# config.ru
require_relative "lib/locale_middleware"
require_relative "app"
use LocaleMiddleware,
available_locales: [:en, :fr, :de, :ja],
default_locale: :en
run AppAccess the detected locale in your app:
# In any Rack app
locale = env["rack.locale"]
# => :fr# app.rb
require "sinatra"
require "accept_language"
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 if header.nil?
AcceptLanguage.parse(header).match(*@available_locales)
end
end
class App < Sinatra::Base
use LocaleMiddleware,
available_locales: [:en, :fr, :de],
default_locale: :en
get "/" do
locale = env["rack.locale"]
"Your locale is: #{locale}"
end
endFor simpler apps, use a before filter:
# app.rb
require "sinatra"
require "accept_language"
AVAILABLE_LOCALES = [:en, :fr, :de, :ja]
DEFAULT_LOCALE = :en
helpers do
def current_locale
@locale ||= detect_locale
end
def detect_locale
header = request.env["HTTP_ACCEPT_LANGUAGE"]
return DEFAULT_LOCALE if header.nil?
AcceptLanguage.parse(header).match(*AVAILABLE_LOCALES) || DEFAULT_LOCALE
end
end
before do
@locale = current_locale
end
get "/" do
"Your locale is: #{current_locale}"
endCombine with the i18n gem for translations:
# app.rb
require "sinatra"
require "sinatra/i18n"
require "accept_language"
class App < Sinatra::Base
register Sinatra::I18n
set :locales, ["config/locales/en.yml", "config/locales/fr.yml"]
set :available_locales, [:en, :fr]
set :default_locale, :en
before do
I18n.locale = detect_locale
end
helpers do
def detect_locale
header = request.env["HTTP_ACCEPT_LANGUAGE"]
return settings.default_locale if header.nil?
AcceptLanguage.parse(header).match(*settings.available_locales) ||
settings.default_locale
end
end
get "/" do
t(:welcome_message)
end
endCreate an action mixin:
# app/actions/locale_detection.rb
require "accept_language"
module LocaleDetection
AVAILABLE_LOCALES = [:en, :fr, :de].freeze
DEFAULT_LOCALE = :en
def detect_locale(request)
header = request.env["HTTP_ACCEPT_LANGUAGE"]
return DEFAULT_LOCALE if header.nil?
AcceptLanguage.parse(header).match(*AVAILABLE_LOCALES) || DEFAULT_LOCALE
end
endUse it in actions:
# app/actions/home/index.rb
module MyApp
module Actions
module Home
class Index < MyApp::Action
include LocaleDetection
def handle(request, response)
locale = detect_locale(request)
response.body = "Your locale is: #{locale}"
end
end
end
end
end# config/app.rb
module MyApp
class App < Hanami::App
config.middleware.use LocaleMiddleware,
available_locales: [:en, :fr, :de],
default_locale: :en
end
end# app.rb
require "roda"
require "accept_language"
class App < Roda
AVAILABLE_LOCALES = [:en, :fr, :de, :ja]
DEFAULT_LOCALE = :en
def detect_locale
header = env["HTTP_ACCEPT_LANGUAGE"]
return DEFAULT_LOCALE if header.nil?
AcceptLanguage.parse(header).match(*AVAILABLE_LOCALES) || DEFAULT_LOCALE
end
route do |r|
@locale = detect_locale
r.root do
"Your locale is: #{@locale}"
end
end
endOr use the middleware:
# config.ru
require_relative "lib/locale_middleware"
require_relative "app"
use LocaleMiddleware,
available_locales: [:en, :fr, :de, :ja],
default_locale: :en
run App.freeze.appCheck multiple sources for locale:
# lib/locale_middleware.rb
require "accept_language"
class LocaleMiddleware
def initialize(app, available_locales:, default_locale:)
@app = app
@available_locales = available_locales
@default_locale = default_locale
end
def call(env)
request = Rack::Request.new(env)
locale = detect_locale(request) || @default_locale
env["rack.locale"] = locale
@app.call(env)
end
private
def detect_locale(request)
locale_from_params(request) ||
locale_from_cookie(request) ||
locale_from_header(request)
end
def locale_from_params(request)
locale = request.params["locale"]
return unless locale
validate_locale(locale)
end
def locale_from_cookie(request)
locale = request.cookies["locale"]
return unless locale
validate_locale(locale)
end
def locale_from_header(request)
header = request.env["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*@available_locales)
end
def validate_locale(locale)
sym = locale.to_sym
return sym if @available_locales.include?(sym)
nil
end
endFor thread-safe access across your app:
# lib/locale_middleware.rb
require "accept_language"
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
# Store in thread-local for easy access
Thread.current[:locale] = locale
begin
@app.call(env)
ensure
Thread.current[:locale] = nil
end
end
private
def detect_locale(env)
header = env["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*@available_locales)
end
end
# Helper module
module Locale
def self.current
Thread.current[:locale]
end
endUsage anywhere:
Locale.current
# => :frFor a minimal Rack app:
# config.ru
require "accept_language"
AVAILABLE_LOCALES = [:en, :fr, :de]
DEFAULT_LOCALE = :en
app = lambda do |env|
header = env["HTTP_ACCEPT_LANGUAGE"]
locale = if header
AcceptLanguage.parse(header).match(*AVAILABLE_LOCALES) || DEFAULT_LOCALE
else
DEFAULT_LOCALE
end
[
200,
{ "Content-Type" => "text/plain" },
["Your locale is: #{locale}"]
]
end
run appInclude Vary header for proper caching:
# lib/locale_middleware.rb
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
status, headers, body = @app.call(env)
# Add Vary header for caching
vary = headers["Vary"]
headers["Vary"] = vary ? "#{vary}, Accept-Language" : "Accept-Language"
[status, headers, body]
end
private
def detect_locale(env)
header = env["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*@available_locales)
end
endPersist user choice:
# lib/locale_middleware.rb
require "accept_language"
require "rack"
class LocaleMiddleware
COOKIE_NAME = "locale"
COOKIE_MAX_AGE = 365 * 24 * 60 * 60 # 1 year
def initialize(app, available_locales:, default_locale:)
@app = app
@available_locales = available_locales
@default_locale = default_locale
end
def call(env)
request = Rack::Request.new(env)
locale = detect_locale(request) || @default_locale
env["rack.locale"] = locale
status, headers, body = @app.call(env)
# Set cookie if from header detection (not already in cookie)
unless request.cookies[COOKIE_NAME]
Rack::Utils.set_cookie_header!(
headers,
COOKIE_NAME,
{ value: locale.to_s, max_age: COOKIE_MAX_AGE, path: "/" }
)
end
[status, headers, body]
end
private
def detect_locale(request)
locale_from_cookie(request) || locale_from_header(request)
end
def locale_from_cookie(request)
locale = request.cookies[COOKIE_NAME]
return unless locale
sym = locale.to_sym
return sym if @available_locales.include?(sym)
nil
end
def locale_from_header(request)
header = request.env["HTTP_ACCEPT_LANGUAGE"]
return if header.nil?
AcceptLanguage.parse(header).match(*@available_locales)
end
endTest your middleware with Rack::Test:
# spec/locale_middleware_spec.rb
require "rack/test"
require_relative "../lib/locale_middleware"
RSpec.describe LocaleMiddleware do
include Rack::Test::Methods
let(:inner_app) do
lambda { |env| [200, {}, [env["rack.locale"].to_s]] }
end
let(:app) do
LocaleMiddleware.new(
inner_app,
available_locales: [:en, :fr, :de],
default_locale: :en
)
end
it "detects French" do
header "Accept-Language", "fr, en;q=0.8"
get "/"
expect(last_response.body).to eq("fr")
end
it "falls back to default" do
header "Accept-Language", "ja"
get "/"
expect(last_response.body).to eq("en")
end
it "handles missing header" do
get "/"
expect(last_response.body).to eq("en")
end
it "respects quality values" do
header "Accept-Language", "de;q=0.7, fr;q=0.9"
get "/"
expect(last_response.body).to eq("fr")
end
end- Real-World Examples — Practical patterns and use cases
- Understanding Quality Values — How preferences are ranked
- Basic Filtering Explained — The matching algorithm