UtopiaSourceUtopiaLocalizationMiddleware

class Middleware

Selects a locale for each request and rewrites localized 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: [])
	@app = app
	
	@all_locales = HTTP::Accept::Languages::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 || options[:nonlocalized]
	
	@methods = methods
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(env)

Compute the preferred locales for the Rack environment.

Signature

parameter env Hash

The Rack environment.

yields {|env| ...}

Each localized environment in preference order.

returns Enumerator | Array

An enumerator when no block is given, otherwise the configured default locales.

Implementation

def preferred_locales(env)
	return to_enum(:preferred_locales, env) unless block_given?
	
	# Keep track of what locales have been tried:
	locales = Set.new
	
	host_preferred_locales(env) do |locale|
		yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
	end
	
	request_preferred_locale(env) do |locale, path|
		# We have extracted a locale from the path, so from this point on we should use the updated path:
		env = env.merge(Rack::PATH_INFO => path.to_s)
		
		yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
	end
	
	browser_preferred_locales(env).each do |locale|
		yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
	end
	
	@default_locales.each do |locale|
		yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
	end
end

def host_preferred_locales(env)

Infer preferred locales from the request host.

Signature

parameter env Hash

The Rack environment.

yields {|locale| ...}

Each locale whose host pattern matches.

returns Hash

The configured host mappings.

Implementation

def host_preferred_locales(env)
	http_host = env[Rack::HTTP_HOST]
	
	# Yield all hosts which match the incoming http_host:
	@hosts.each do |pattern, locale|
		yield locale if http_host[pattern]
	end
end

def request_preferred_locale(env)

Select the preferred locale from the request path.

Signature

parameter env Hash

The Rack environment.

yields {|locale, path| ...}

The locale and path without its locale prefix.

returns Object | Nil

The block result when a locale prefix is present.

Implementation

def request_preferred_locale(env)
	path = Path[env[Rack::PATH_INFO]]
	
	if request_locale = @all_locales.patterns[path.first]
		# Remove the localization prefix:
		path.delete_at(0)
		
		yield request_locale, path
	end
end

def browser_preferred_locales(env)

Parse the locales preferred by the browser.

Signature

parameter env Hash

The Rack environment.

returns Array(String)

Supported locales in preference order.

Implementation

def browser_preferred_locales(env)
	accept_languages = env[HTTP_ACCEPT_LANGUAGE]
	
	# No user prefered languages:
	return [] unless accept_languages
	
	# Extract the ordered list of languages:
	languages = HTTP::Accept::Languages.parse(accept_languages)
	
	# Returns available languages based on the order languages:
	return @all_locales & languages
rescue HTTP::Accept::ParseError
	# If we fail to parse the browser Accept-Language header, we ignore it (silently).
	return []
end

def localized?(env)

Check whether the request path is eligible for localization.

Signature

parameter env Hash

The Rack environment.

returns Boolean

Whether the path is eligible for localization.

Implementation

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

def vary(env, response)

Set the Vary: header on the response to indicate that this response should include the header in the cache key.

Implementation

def vary(env, response)
	headers = response[1].to_a
	
	# This response was based on the Accept-Language header:
	headers << ["Vary", "Accept-Language"]
	
	# Althought this header is generally not supported, we supply it anyway as it is useful for debugging:
	if locale = env[CURRENT_LOCALE_KEY]
		# Set the Content-Location to point to the localized URI as requested:
		headers["Content-Location"] = "/#{locale}" + env[Rack::PATH_INFO]
	end
	
	return response
end

def call(env)

Try the preferred locales until the application returns a successful response.

Signature

parameter env Hash

The Rack environment.

returns Array

The localized Rack response.

Implementation

def call(env)
	# Pass the request through if it shouldn't be localized:
	return @app.call(env) unless localized?(env)
	
	env[LOCALIZATION_KEY] = self
	
	response = nil
	
	# We have a non-localized request, but there might be a localized resource. We return the best localization possible:
	preferred_locales(env) do |localized_env|
		# puts "Trying locale: #{localized_env[CURRENT_LOCALE_KEY]}: #{localized_env[Rack::PATH_INFO]}..."
		
		response = @app.call(localized_env)
		
		break unless response[0] >= 400
		
		response[2].close if response[2].respond_to?(:close)
	end
	
	return vary(env, response)
end