Class: Rack::I18nBestLangs

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/i18n_best_langs.rb

Constant Summary collapse

RACK_VARIABLE =
'rack.i18n_best_langs'.freeze
DEFAULT_WEIGHTS =
{
	:header => 1,
	:aliases_path => 2,
	:path => 3,
	:cookie => 4,
}.freeze
HEADER_FORMAT =
self.accept_language_format.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, avail_languages, opts = {}) ⇒ I18nBestLangs

Create a new I18nBestLangs middleware component.

Parameters:

  • avail_languages ([String])
  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :weights (Hash{Symbol => Integer})

    Weights for clues (the higher, the most important): :header, :path, :cookie, :aliases_path.

  • :path_mapping_fn (#map_with_langs)

    A function that maps localized URI paths into normalized paths, should be a Rack::I18nRoutes::AliasMapping.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/rack/i18n_best_langs.rb', line 35

def initialize(app, avail_languages, opts = {})
	@app = app

	score_base = avail_languages.length

	weights = opts[:weights] || DEFAULT_WEIGHTS
	@score_for_header       = score_base * (10 ** weights[:header])
	@score_for_aliases_path = score_base * (10 ** weights[:aliases_path])
	@score_for_path         = score_base * (10 ** weights[:path])
	@score_for_cookie       = score_base * (10 ** weights[:cookie])

	@avail_languages = {}
	avail_languages.each_with_index do |lang, i|
		code = LanguageTag.new(lang).freeze
		score = score_base - i

		@avail_languages[code] = score
	end
	@avail_languages.freeze

	@language_path_regex = regex_for_languages_in_path.freeze

	@path_mapping_fn = opts[:path_mapping_fn]
end

Class Method Details

.accept_language_formatObject



206
207
208
209
210
211
# File 'lib/rack/i18n_best_langs.rb', line 206

def self.accept_language_format
	lang = '[-_a-zA-Z]+'
	qvalue = '(; ?q=[01]+(\.[0-9]{1,3})?)'

	return Regexp.new("\\A#{lang}#{qvalue}?(, ?#{lang}#{qvalue}?)*\\Z")
end

Instance Method Details

#add_score_for_accept_language_header(accept_language_header, langs) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/rack/i18n_best_langs.rb', line 123

def add_score_for_accept_language_header(accept_language_header, langs)
	if accept_language_header.nil? || !valid_language_header(accept_language_header)
		return
	end

	header_langs = languages_in_accept_language(accept_language_header)

	header_langs.each do |lang, q|
		if !langs.include?(lang)
			next
		end

		langs[lang] += @score_for_header * q
	end
end

#add_score_for_aliases_path(path, langs) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/rack/i18n_best_langs.rb', line 156

def add_score_for_aliases_path(path, langs)
	if !@path_mapping_fn.respond_to?(:path_analysis)
		return
	end

	ph, translation, aliases_langs = @path_mapping_fn.path_analysis(path)
	aliases_langs.map! { |tag| LanguageTag.parse(tag) }

	lang_uses = aliases_langs.inject(Hash.new(0)) {|freq, lang| freq[lang] += 1; freq }
	lang_uses.sort_by { |lang, freq| -freq }.each do |lang, freq|
		if !langs.include?(lang)
			next
		end

		langs[lang] += @score_for_aliases_path * freq
	end
end


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rack/i18n_best_langs.rb', line 139

def add_score_for_cookie(cookie, langs)
	if cookie.nil?
		return
	end

	cookie_langs = cookie.split(',').map { |tag| LanguageTag.parse(tag) }

	cookie_langs.reverse.each_with_index do |lang, idx|
		if !langs.include?(lang)
			next
		end

		importance = idx + 1
		langs[lang] += @score_for_cookie * importance
	end
end

#add_score_for_path(path, langs) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
# File 'lib/rack/i18n_best_langs.rb', line 111

def add_score_for_path(path, langs)
	path_match = path.match(@language_path_regex)

	path_include_language = !path_match.nil?
	if !path_include_language
		return
	end

	lang_code = LanguageTag.new(path_match[1])
	langs[lang_code] += @score_for_path
end

#call(env) ⇒ Object



60
61
62
63
64
65
66
67
# File 'lib/rack/i18n_best_langs.rb', line 60

def call(env)
	lang_info = find_best_languages(env)

	env[RACK_VARIABLE] = lang_info[:languages]
	env['PATH_INFO'] = lang_info[:path_info]

	return @app.call(env)
end


103
104
105
# File 'lib/rack/i18n_best_langs.rb', line 103

def extract_language_cookie(env)
	return Rack::Request.new(env).cookies[RACK_VARIABLE]
end

#extract_language_header(env) ⇒ Object



92
93
94
95
96
97
98
99
100
101
# File 'lib/rack/i18n_best_langs.rb', line 92

def extract_language_header(env)
	header = env['HTTP_ACCEPT_LANGUAGE']

	if !(header =~ HEADER_FORMAT)
		env["rack.errors"].puts("Warning: malformed Accept-Language header")
		return nil
	end

	return header
end

#find_best_languages(env) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rack/i18n_best_langs.rb', line 69

def find_best_languages(env)
	path = env['PATH_INFO']
	accept_language_header = extract_language_header(env)
	cookies = extract_language_cookie(env)

	clean_path_info = remove_language_from_path(path)

	langs = @avail_languages.dup
	add_score_for_path(path, langs)
	add_score_for_accept_language_header(accept_language_header, langs)
	add_score_for_cookie(cookies, langs)
	add_score_for_aliases_path(path, langs)

	sorted_langs = langs.to_a.sort_by { |lang_info| -(lang_info[1]) }.map(&:first)

	info = {
		:languages => sorted_langs,
		:path_info => clean_path_info,
	}

	return info
end

#languages_in_accept_language(accept_language_header) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/rack/i18n_best_langs.rb', line 178

def languages_in_accept_language(accept_language_header)
	raw_langs = accept_language_header.split(',')

	langs = raw_langs.map { |l| l.sub('q=', '')}.
	                      map { |l| l.split(';') }

	langs.each_with_index do |l, i|
		l[0] = LanguageTag.parse(l[0])
		l[1] = (l[1] || 1).to_f

		sorting_epsilon = (langs.size - i).to_f / 100
		l[1] += sorting_epsilon # keep the original order when sorting
	end

	return langs
end

#regex_for_languages_in_pathObject



195
196
197
198
199
200
201
202
203
# File 'lib/rack/i18n_best_langs.rb', line 195

def regex_for_languages_in_path
	all_languages = @avail_languages.keys.map(&:alpha3)

	preamble = "/"
	body = "(" + all_languages.join("|") + ")"
	trail = "/?$"

	return Regexp.new(preamble + body + trail)
end

#remove_language_from_path(path) ⇒ Object



107
108
109
# File 'lib/rack/i18n_best_langs.rb', line 107

def remove_language_from_path(path)
	return path.sub(@language_path_regex, '')
end

#valid_language_header(accept_language_header) ⇒ Object



174
175
176
# File 'lib/rack/i18n_best_langs.rb', line 174

def valid_language_header(accept_language_header)
	return true # FIXME: check with regex
end