class Middleware < Protocol::HTTP::Middleware

Inherits from
Protocol::HTTP::Middleware

Computes localization preferences and rewrites locale-prefixed paths.

Definitions

def initialize(app, locales:, default_locale: nil, default_locales: nil, hosts: {}, ignore: [])

Implementation

def initialize(app, locales:, default_locale: nil, default_locales: nil, hosts: {}, ignore: [])
	super(app)
	
	@all_locales = Locales.new(locales)
	
	# Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'] and are used in order if no locale is specified by the user.
	unless @default_locales = default_locales
		if default_locale
			@default_locales = [default_locale, nil]
		else
			# We append nil, i.e. no localization.
			@default_locales = @all_locales.names + [nil]
		end
	end
	
	@default_locale = default_locale || @default_locales.first
	
	unless @default_locales.include? @default_locale
		@default_locales.unshift(@default_locale)
	end
	
	# Select a localization based on a request host name:
	@hosts = hosts
	
	@ignore = ignore
end

def freeze

Freeze this object and its internal state.

Signature

returns self

This object.

Implementation

def freeze
	return self if frozen?
	
	@all_locales.freeze
	@default_locales.freeze
	@default_locale.freeze
	@hosts.freeze
	@ignore.freeze
	
	super
end

def preferred_locales(request, path_locale = nil)

Compute the preferred locales for a request.

Signature

parameter request Utopia::Request

The application request.

parameter path_locale String | Nil

The locale extracted from the request path.

returns Array(String | Nil)

The unique locales in preference order.

Implementation

def preferred_locales(request, path_locale = nil)
	# Keep track of what locales have been tried:
	locales = Set.new
	
	if path_locale
		locales.add(path_locale)
	end
	
	host_preferred_locales(request) do |locale|
		locales.add(locale)
	end
	
	browser_preferred_locales(request).each do |locale|
		locales.add(locale)
	end
	
	@default_locales.each do |locale|
		locales.add(locale)
	end
	
	return locales.to_a
end

def host_preferred_locales(request)

Infer preferred locales from the request authority.

Signature

parameter request Utopia::Request

The application request.

yields {|locale| ...}

Each locale whose host pattern matches the request authority.

returns Hash

The configured host mappings.

Implementation

def host_preferred_locales(request)
	authority = request.authority.to_s
	
	# Yield all hosts which match the incoming authority:
	@hosts.each do |pattern, locale|
		if authority[pattern]
			yield locale
		end
	end
end

def extract_path_locale(request)

Extract a locale prefix from the request path.

Signature

parameter request Utopia::Request

The application request.

returns Array(Utopia::Request, String | Nil)

The request and extracted locale.

Implementation

def extract_path_locale(request)
	path = request.url.path
	
	# Localization prefixes only apply to absolute application paths:
	unless path.absolute?
		return request, nil
	end
	
	if segment = path.segments[1]
		# Decode only the component which may contain the locale:
		component = Protocol::URL::Encoding::System.unescape(segment)
		
		if request_locale = @all_locales.patterns[component]
			# Remove the locale while preserving all other encoded segments:
			segments = path.segments.dup
			segments.delete_at(1)
			
			# Preserve the absolute root when the locale was the only component:
			if segments == [""]
				segments << ""
			end
			
			path = Protocol::URL::Path.new(nil, segments)
			
			return request.with(path: path), request_locale
		end
	end
	
	return request, nil
end

def browser_preferred_locales(request)

Parse the locales preferred by the browser.

Signature

parameter request Utopia::Request

The application request.

returns Array(String)

Supported locales accepted by the browser, in preference order.

Implementation

def browser_preferred_locales(request)
	accept_languages = request.headers["accept-language"]
	
	# No user prefered languages:
	return [] unless accept_languages
	
	# Extract the ordered list of languages:
	languages = accept_languages.preferred_languages
	
	# Returns available languages based on the order languages:
	return @all_locales.match(languages)
rescue Protocol::HTTP::Header::AcceptLanguage::ParseError
	# If we fail to parse the browser Accept-Language header, we ignore it (silently).
	return []
end

def localized?(request)

Check whether the request path includes a locale.

Signature

parameter request Utopia::Request

The application request.

returns Boolean

Whether the path is eligible for localization.

Implementation

def localized?(request)
	# Ignore requests which match the ignored paths:
	path = request.url.path.encoded
	return false if @ignore.any?{|pattern| path[pattern] != nil}
	
	return true
end

def vary(response)

Mark the response as varying by language.

Signature

parameter response Protocol::HTTP::Response

The response.

returns Protocol::HTTP::Response

The response with localization headers.

Implementation

def vary(response)
	response = Response.wrap(response)
	headers = response.headers
	
	# This response was based on the Accept-Language header:
	headers.add("vary", "Accept-Language")
	
	return response
end

def call(request)

Attach localization preferences and invoke the application once.

Signature

parameter request Utopia::Request

The request.

returns Protocol::HTTP::Response

The response with cache-variation headers.

Implementation

def call(request)
	# Pass the request through if it shouldn't be localized:
	return @delegate.call(request) unless localized?(request)
	
	request, path_locale = extract_path_locale(request)
	locales = preferred_locales(request, path_locale)
	
	request.localization = Preferences.new(
		all_locales: @all_locales.names,
		preferred_locales: locales,
		default_locale: @default_locale,
	)
	
	return vary(@delegate.call(request))
end