rack-i18n_best_langs 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +4 -0
- data/COPYING +121 -0
- data/README.md +176 -0
- data/Rakefile +30 -0
- data/lib/rack/i18n_best_langs.rb +213 -0
- data/lib/rack/language_tag.rb +84 -0
- data/spec/i18n-best-langs_spec.rb +132 -0
- data/spec/spec_helper.rb +18 -0
- metadata +153 -0
data/.yardopts
ADDED
data/COPYING
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
Creative Commons Legal Code
|
2
|
+
|
3
|
+
CC0 1.0 Universal
|
4
|
+
|
5
|
+
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
6
|
+
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
7
|
+
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
8
|
+
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
9
|
+
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
10
|
+
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
11
|
+
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
12
|
+
HEREUNDER.
|
13
|
+
|
14
|
+
Statement of Purpose
|
15
|
+
|
16
|
+
The laws of most jurisdictions throughout the world automatically confer
|
17
|
+
exclusive Copyright and Related Rights (defined below) upon the creator
|
18
|
+
and subsequent owner(s) (each and all, an "owner") of an original work of
|
19
|
+
authorship and/or a database (each, a "Work").
|
20
|
+
|
21
|
+
Certain owners wish to permanently relinquish those rights to a Work for
|
22
|
+
the purpose of contributing to a commons of creative, cultural and
|
23
|
+
scientific works ("Commons") that the public can reliably and without fear
|
24
|
+
of later claims of infringement build upon, modify, incorporate in other
|
25
|
+
works, reuse and redistribute as freely as possible in any form whatsoever
|
26
|
+
and for any purposes, including without limitation commercial purposes.
|
27
|
+
These owners may contribute to the Commons to promote the ideal of a free
|
28
|
+
culture and the further production of creative, cultural and scientific
|
29
|
+
works, or to gain reputation or greater distribution for their Work in
|
30
|
+
part through the use and efforts of others.
|
31
|
+
|
32
|
+
For these and/or other purposes and motivations, and without any
|
33
|
+
expectation of additional consideration or compensation, the person
|
34
|
+
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
35
|
+
is an owner of Copyright and Related Rights in the Work, voluntarily
|
36
|
+
elects to apply CC0 to the Work and publicly distribute the Work under its
|
37
|
+
terms, with knowledge of his or her Copyright and Related Rights in the
|
38
|
+
Work and the meaning and intended legal effect of CC0 on those rights.
|
39
|
+
|
40
|
+
1. Copyright and Related Rights. A Work made available under CC0 may be
|
41
|
+
protected by copyright and related or neighboring rights ("Copyright and
|
42
|
+
Related Rights"). Copyright and Related Rights include, but are not
|
43
|
+
limited to, the following:
|
44
|
+
|
45
|
+
i. the right to reproduce, adapt, distribute, perform, display,
|
46
|
+
communicate, and translate a Work;
|
47
|
+
ii. moral rights retained by the original author(s) and/or performer(s);
|
48
|
+
iii. publicity and privacy rights pertaining to a person's image or
|
49
|
+
likeness depicted in a Work;
|
50
|
+
iv. rights protecting against unfair competition in regards to a Work,
|
51
|
+
subject to the limitations in paragraph 4(a), below;
|
52
|
+
v. rights protecting the extraction, dissemination, use and reuse of data
|
53
|
+
in a Work;
|
54
|
+
vi. database rights (such as those arising under Directive 96/9/EC of the
|
55
|
+
European Parliament and of the Council of 11 March 1996 on the legal
|
56
|
+
protection of databases, and under any national implementation
|
57
|
+
thereof, including any amended or successor version of such
|
58
|
+
directive); and
|
59
|
+
vii. other similar, equivalent or corresponding rights throughout the
|
60
|
+
world based on applicable law or treaty, and any national
|
61
|
+
implementations thereof.
|
62
|
+
|
63
|
+
2. Waiver. To the greatest extent permitted by, but not in contravention
|
64
|
+
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
65
|
+
irrevocably and unconditionally waives, abandons, and surrenders all of
|
66
|
+
Affirmer's Copyright and Related Rights and associated claims and causes
|
67
|
+
of action, whether now known or unknown (including existing as well as
|
68
|
+
future claims and causes of action), in the Work (i) in all territories
|
69
|
+
worldwide, (ii) for the maximum duration provided by applicable law or
|
70
|
+
treaty (including future time extensions), (iii) in any current or future
|
71
|
+
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
72
|
+
including without limitation commercial, advertising or promotional
|
73
|
+
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
74
|
+
member of the public at large and to the detriment of Affirmer's heirs and
|
75
|
+
successors, fully intending that such Waiver shall not be subject to
|
76
|
+
revocation, rescission, cancellation, termination, or any other legal or
|
77
|
+
equitable action to disrupt the quiet enjoyment of the Work by the public
|
78
|
+
as contemplated by Affirmer's express Statement of Purpose.
|
79
|
+
|
80
|
+
3. Public License Fallback. Should any part of the Waiver for any reason
|
81
|
+
be judged legally invalid or ineffective under applicable law, then the
|
82
|
+
Waiver shall be preserved to the maximum extent permitted taking into
|
83
|
+
account Affirmer's express Statement of Purpose. In addition, to the
|
84
|
+
extent the Waiver is so judged Affirmer hereby grants to each affected
|
85
|
+
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
86
|
+
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
87
|
+
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
88
|
+
maximum duration provided by applicable law or treaty (including future
|
89
|
+
time extensions), (iii) in any current or future medium and for any number
|
90
|
+
of copies, and (iv) for any purpose whatsoever, including without
|
91
|
+
limitation commercial, advertising or promotional purposes (the
|
92
|
+
"License"). The License shall be deemed effective as of the date CC0 was
|
93
|
+
applied by Affirmer to the Work. Should any part of the License for any
|
94
|
+
reason be judged legally invalid or ineffective under applicable law, such
|
95
|
+
partial invalidity or ineffectiveness shall not invalidate the remainder
|
96
|
+
of the License, and in such case Affirmer hereby affirms that he or she
|
97
|
+
will not (i) exercise any of his or her remaining Copyright and Related
|
98
|
+
Rights in the Work or (ii) assert any associated claims and causes of
|
99
|
+
action with respect to the Work, in either case contrary to Affirmer's
|
100
|
+
express Statement of Purpose.
|
101
|
+
|
102
|
+
4. Limitations and Disclaimers.
|
103
|
+
|
104
|
+
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
105
|
+
surrendered, licensed or otherwise affected by this document.
|
106
|
+
b. Affirmer offers the Work as-is and makes no representations or
|
107
|
+
warranties of any kind concerning the Work, express, implied,
|
108
|
+
statutory or otherwise, including without limitation warranties of
|
109
|
+
title, merchantability, fitness for a particular purpose, non
|
110
|
+
infringement, or the absence of latent or other defects, accuracy, or
|
111
|
+
the present or absence of errors, whether or not discoverable, all to
|
112
|
+
the greatest extent permissible under applicable law.
|
113
|
+
c. Affirmer disclaims responsibility for clearing rights of other persons
|
114
|
+
that may apply to the Work or any use thereof, including without
|
115
|
+
limitation any person's Copyright and Related Rights in the Work.
|
116
|
+
Further, Affirmer disclaims responsibility for obtaining any necessary
|
117
|
+
consents, permissions or other rights required for any use of the
|
118
|
+
Work.
|
119
|
+
d. Affirmer understands and acknowledges that Creative Commons is not a
|
120
|
+
party to this document and has no duty or obligation with respect to
|
121
|
+
this CC0 or use of the Work.
|
data/README.md
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
rack-i8n_best_langs: guess best language for content served over Rack
|
2
|
+
=====================================================================
|
3
|
+
|
4
|
+
rack-i18n_best_langs is a Rack middleware component that takes care of
|
5
|
+
understanding what are the best languages for a site visitor.
|
6
|
+
|
7
|
+
If you manage a site that has content many languages and also localized URLs,
|
8
|
+
you will find `rack-i18n_best_langs` very useful, especially when used in
|
9
|
+
conjunction with `rack-i18n_routes`.
|
10
|
+
|
11
|
+
|
12
|
+
Features
|
13
|
+
--------
|
14
|
+
|
15
|
+
Language discovery is done using three clues:
|
16
|
+
|
17
|
+
* the presences of language tags in paths (e.g. `/service/warranty/ita`),
|
18
|
+
* the content of the HTTP `Accept-Language` header,
|
19
|
+
* the content of the `rack.i18n_best_langs` cookie when set.
|
20
|
+
|
21
|
+
All these clues are taken into account and evaluated against the list
|
22
|
+
of languages available and their preferred order. It is possible to configure
|
23
|
+
which of these clues is the most important.
|
24
|
+
|
25
|
+
An additional clue is available when `AliasMapping` is used as the mapping
|
26
|
+
function: the language in which the path is written. For
|
27
|
+
example, `/articles/the-victory` is English, `/artículos/la-victoria`, is
|
28
|
+
Spanish, `/articles/la-victoire` is French.
|
29
|
+
|
30
|
+
|
31
|
+
Examples
|
32
|
+
--------
|
33
|
+
|
34
|
+
rack-i18n_best_langs works like any other Rack middleware component.
|
35
|
+
|
36
|
+
# in your server.ru rackup file
|
37
|
+
require 'rack/i18n_best_langs'
|
38
|
+
|
39
|
+
FAVORITE_LANGUAGES = %w(eng spa deu fra)
|
40
|
+
|
41
|
+
use Rack::I18nBestLangs, FAVORITE_LANGUAGES
|
42
|
+
run MyApp
|
43
|
+
|
44
|
+
In your application you will find the list of languages that should be used to
|
45
|
+
serve the content, arranged from the most favorite to the least in the
|
46
|
+
`rack.i18n_best_langs` Rack variable. It is then up to downstream application
|
47
|
+
to use this information in the best way.
|
48
|
+
|
49
|
+
### See the guessed languages
|
50
|
+
|
51
|
+
This small application
|
52
|
+
|
53
|
+
# in your server.ru rackup file
|
54
|
+
require 'rack/i18n_best_langs'
|
55
|
+
|
56
|
+
FAVORITE_LANGUAGES = %w(eng spa deu)
|
57
|
+
|
58
|
+
use Rack::I18nBestLangs, FAVORITE_LANGUAGES
|
59
|
+
|
60
|
+
app = Proc.new do |env|
|
61
|
+
langs = env['rack.i18n_best_langs']
|
62
|
+
[200, {"Content-Type" => "text/plain"}, [langs.inspect] ]
|
63
|
+
end
|
64
|
+
|
65
|
+
run app
|
66
|
+
|
67
|
+
will produce the following results for these URLs.
|
68
|
+
|
69
|
+
# /foo =>
|
70
|
+
# [#<LocaleCode 'eng'>, #<LocaleCode 'spa'>, #<LocaleCode 'deu'>]
|
71
|
+
|
72
|
+
# /foo/spa =>
|
73
|
+
# [#<LocaleCode 'spa'>, #<LocaleCode 'eng'>, #<LocaleCode 'deu'>]
|
74
|
+
|
75
|
+
# /foo (with Accept-Language = it-IT, es-ES, fr-FR) =>
|
76
|
+
# [#<LocaleCode 'spa'>, #<LocaleCode 'eng'>, #<LocaleCode 'deu'>]
|
77
|
+
|
78
|
+
# /foo/deu (with Accept-Language = it-IT, es-ES) =>
|
79
|
+
# [#<LocaleCode 'deu'>, #<LocaleCode 'spa'>, #<LocaleCode 'eng'>]
|
80
|
+
|
81
|
+
# /foo (with cookie set to 'deu') =>
|
82
|
+
# [#<LocaleCode 'deu'>, #<LocaleCode 'eng'>, #<LocaleCode 'spa'>]
|
83
|
+
|
84
|
+
# /foo/spa (with cookie set to 'deu') =>
|
85
|
+
# [#<LocaleCode 'deu'>, #<LocaleCode 'spa'>, #<LocaleCode 'eng'>]
|
86
|
+
|
87
|
+
|
88
|
+
### Changing the clues' weights
|
89
|
+
|
90
|
+
You can tune the weights of the clues to set which clue is the most important.
|
91
|
+
|
92
|
+
The default order of importance and weights are
|
93
|
+
|
94
|
+
* language set in cookie (`:cookie`): 3
|
95
|
+
* language present in tag (`:path`): 2
|
96
|
+
* language is in `Accept-Language` header (`:header`): 1
|
97
|
+
|
98
|
+
You can change these weight with the `:weights` option.
|
99
|
+
|
100
|
+
FAVORITE_LANGUAGES = %w(eng spa deu)
|
101
|
+
WEIGHTS = { :path => 3, :header => 2, :cookie = 1 }
|
102
|
+
|
103
|
+
use Rack::I18nBestLangs, FAVORITE_LANGUAGES, :weights => WEIGHTS
|
104
|
+
|
105
|
+
|
106
|
+
### Using `AliasMapping`
|
107
|
+
|
108
|
+
If you want to use the content of the URI path as an additional clue to guess
|
109
|
+
the best languages, use an `AliasMapping` function as path mapping function.
|
110
|
+
|
111
|
+
# in your server.ru rackup file
|
112
|
+
require 'rack/i18n_best_langs'
|
113
|
+
require 'rack/i18n_routes/alias_mapping'
|
114
|
+
|
115
|
+
FAVORITE_LANGUAGES = %w(eng spa deu fra)
|
116
|
+
|
117
|
+
aliases = {
|
118
|
+
'articles' => {
|
119
|
+
'fra' => 'articles',
|
120
|
+
'spa' => ['artículos', 'articulos']
|
121
|
+
|
122
|
+
:children => {
|
123
|
+
'the-victory' => {
|
124
|
+
'fra' => 'la-victoire',
|
125
|
+
'spa' => 'la-victoria'
|
126
|
+
}
|
127
|
+
'the-block' => {
|
128
|
+
'fra' => 'le-bloc',
|
129
|
+
'spa' => 'el-bloque'
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
MAPPING = Rack::I18nRoutes::AliasMapping.new(paths, :default => 'eng')
|
135
|
+
|
136
|
+
use Rack::I18nBestLangs, FAVORITE_LANGUAGES, :path_mapping_fn => MAPPING
|
137
|
+
run MyApp
|
138
|
+
|
139
|
+
|
140
|
+
Requirements
|
141
|
+
------------
|
142
|
+
|
143
|
+
No requirements outside Ruby >= 1.8.7 and Rack.
|
144
|
+
|
145
|
+
|
146
|
+
Install
|
147
|
+
-------
|
148
|
+
|
149
|
+
gem install rack-i18n_best_langs
|
150
|
+
|
151
|
+
|
152
|
+
Author
|
153
|
+
------
|
154
|
+
|
155
|
+
* Gioele Barabucci <http://svario.it/gioele> (initial author)
|
156
|
+
|
157
|
+
Development
|
158
|
+
-----------
|
159
|
+
|
160
|
+
Code
|
161
|
+
: <https://github.com/gioele/rack-i18n_best_langs>
|
162
|
+
|
163
|
+
Report issues
|
164
|
+
: <https://github.com/gioele/rack-i18n_best_langs/issues>
|
165
|
+
|
166
|
+
Documentation
|
167
|
+
: <http://rubydoc.info/gems/rack-i18n_best_langs>
|
168
|
+
|
169
|
+
|
170
|
+
License
|
171
|
+
-------
|
172
|
+
|
173
|
+
This is free software released into the public domain (CC0 license).
|
174
|
+
|
175
|
+
See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
|
176
|
+
for more details.
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# This is free software released into the public domain (CC0 license).
|
2
|
+
#
|
3
|
+
# See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
|
4
|
+
# for more details.
|
5
|
+
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'bones'
|
9
|
+
rescue LoadError
|
10
|
+
abort '### Please install the "bones" gem ###'
|
11
|
+
end
|
12
|
+
|
13
|
+
Bones {
|
14
|
+
name 'rack-i18n_best_langs'
|
15
|
+
authors 'Gioele Barabucci'
|
16
|
+
email 'gioele@svario.it'
|
17
|
+
url 'https://github.com/gioele/rack-i18n_best_langs'
|
18
|
+
|
19
|
+
version '0.2'
|
20
|
+
|
21
|
+
ignore_file '.gitignore'
|
22
|
+
|
23
|
+
depend_on 'rack'
|
24
|
+
depend_on 'rack-test', :development => true
|
25
|
+
depend_on 'rack-i18n_routes', :development => true
|
26
|
+
depend_on 'bones-rspec', :development => true
|
27
|
+
}
|
28
|
+
|
29
|
+
task :default => 'spec:run'
|
30
|
+
task 'gem:release' => 'spec:run'
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# This is free software released into the public domain (CC0 license).
|
2
|
+
#
|
3
|
+
# See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
|
4
|
+
# for more details.
|
5
|
+
|
6
|
+
|
7
|
+
require 'rack/language_tag.rb'
|
8
|
+
|
9
|
+
module Rack
|
10
|
+
|
11
|
+
class I18nBestLangs
|
12
|
+
RACK_VARIABLE = 'rack.i18n_best_langs'
|
13
|
+
|
14
|
+
# Create a new I18nBestLangs middleware component.
|
15
|
+
#
|
16
|
+
# @param [[String]] avail_languages
|
17
|
+
#
|
18
|
+
# @param [Hash] opts
|
19
|
+
# @option opts [Hash{Symbol => Integer}] :weights Weights for clues
|
20
|
+
# (the higher, the most
|
21
|
+
# important): `:header`,
|
22
|
+
# `:path`, `:cookie`, `:aliases_path`.
|
23
|
+
# @option opts [#map_with_langs] :path_mapping_fn A function that maps
|
24
|
+
# localized URI paths
|
25
|
+
# into normalized paths,
|
26
|
+
# should be a
|
27
|
+
# Rack::I18nRoutes::AliasMapping.
|
28
|
+
|
29
|
+
def initialize(app, avail_languages, opts = {})
|
30
|
+
@app = app
|
31
|
+
|
32
|
+
score_base = avail_languages.length
|
33
|
+
|
34
|
+
weights = opts[:weights] || {}
|
35
|
+
weight_header = weights[:header] || 1
|
36
|
+
weight_aliases_path = weights[:aliases_path] || 2
|
37
|
+
weight_path = weights[:path] || 3
|
38
|
+
weight_cookie = weights[:cookie] || 4
|
39
|
+
|
40
|
+
@score_for_header = score_base * (10 ** weight_header)
|
41
|
+
@score_for_aliases_path = score_base * (10 ** weight_aliases_path)
|
42
|
+
@score_for_path = score_base * (10 ** weight_path)
|
43
|
+
@score_for_cookie = score_base * (10 ** weight_cookie)
|
44
|
+
|
45
|
+
@avail_languages = {}
|
46
|
+
avail_languages.each_with_index do |lang, i|
|
47
|
+
code = LanguageTag.new(lang).freeze
|
48
|
+
score = score_base - i
|
49
|
+
|
50
|
+
@avail_languages[code] = score
|
51
|
+
end
|
52
|
+
|
53
|
+
@language_path_regex = regex_for_languages_in_path
|
54
|
+
|
55
|
+
@path_mapping_fn = opts[:path_mapping_fn]
|
56
|
+
end
|
57
|
+
|
58
|
+
def call(env)
|
59
|
+
lang_info = find_best_languages(env)
|
60
|
+
env[I18nBestLangs::RACK_VARIABLE] = lang_info[:languages]
|
61
|
+
env['PATH_INFO'] = lang_info[:path_info]
|
62
|
+
|
63
|
+
return @app.call(env)
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_best_languages(env)
|
67
|
+
path = env['PATH_INFO']
|
68
|
+
accept_language_header = extract_language_header(env)
|
69
|
+
cookies = extract_language_cookie(env)
|
70
|
+
|
71
|
+
clean_path_info = remove_language_from_path(path)
|
72
|
+
|
73
|
+
langs = @avail_languages.dup
|
74
|
+
add_score_for_path(path, langs)
|
75
|
+
add_score_for_accept_language_header(accept_language_header, langs)
|
76
|
+
add_score_for_cookie(cookies, langs)
|
77
|
+
add_score_for_aliases_path(path, langs)
|
78
|
+
|
79
|
+
sorted_langs = langs.to_a.sort_by { |lang_info| -(lang_info[1]) }.map(&:first)
|
80
|
+
|
81
|
+
info = {
|
82
|
+
:languages => sorted_langs,
|
83
|
+
:path_info => clean_path_info,
|
84
|
+
}
|
85
|
+
|
86
|
+
return info
|
87
|
+
end
|
88
|
+
|
89
|
+
def extract_language_header(env)
|
90
|
+
header = env['HTTP_ACCEPT_LANGUAGE']
|
91
|
+
|
92
|
+
if (header =~ HEADER_FORMAT)
|
93
|
+
return header
|
94
|
+
else
|
95
|
+
# FIXME: env.warn
|
96
|
+
return ""
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def extract_language_cookie(env)
|
101
|
+
return Rack::Request.new(env).cookies[I18nBestLangs::RACK_VARIABLE]
|
102
|
+
end
|
103
|
+
|
104
|
+
def remove_language_from_path(path)
|
105
|
+
return path.sub(@language_path_regex, '')
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_score_for_path(path, langs)
|
109
|
+
path_match = path.match(@language_path_regex)
|
110
|
+
|
111
|
+
path_include_language = !path_match.nil?
|
112
|
+
if !path_include_language
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
lang_code = LanguageTag.new(path_match[1])
|
117
|
+
langs[lang_code] += @score_for_path
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_score_for_accept_language_header(accept_language_header, langs)
|
121
|
+
if accept_language_header.nil? || !valid_language_header(accept_language_header)
|
122
|
+
return
|
123
|
+
end
|
124
|
+
|
125
|
+
header_langs = languages_in_accept_language(accept_language_header)
|
126
|
+
|
127
|
+
header_langs.each do |lang, q|
|
128
|
+
if !langs.include?(lang)
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
langs[lang] += @score_for_header * q
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_score_for_cookie(cookie, langs)
|
137
|
+
if cookie.nil?
|
138
|
+
return
|
139
|
+
end
|
140
|
+
|
141
|
+
cookie_langs = cookie.split(',').map { |tag| LanguageTag.parse(tag) }
|
142
|
+
|
143
|
+
cookie_langs.reverse.each_with_index do |lang, idx|
|
144
|
+
if !langs.include?(lang)
|
145
|
+
next
|
146
|
+
end
|
147
|
+
|
148
|
+
importance = idx + 1
|
149
|
+
langs[lang] += @score_for_cookie * importance
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def add_score_for_aliases_path(path, langs)
|
154
|
+
if !@path_mapping_fn.respond_to?(:map_with_langs)
|
155
|
+
return
|
156
|
+
end
|
157
|
+
|
158
|
+
ph, aliases_langs = @path_mapping_fn.map_with_langs(path)
|
159
|
+
aliases_langs.map! { |tag| LanguageTag.parse(tag) }
|
160
|
+
|
161
|
+
lang_uses = aliases_langs.inject(Hash.new(0)) {|freq, lang| freq[lang] += 1; freq }
|
162
|
+
lang_uses.sort_by { |lang, freq| -freq }.each do |lang, freq|
|
163
|
+
if !langs.include?(lang)
|
164
|
+
next
|
165
|
+
end
|
166
|
+
|
167
|
+
langs[lang] += @score_for_aliases_path * freq
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def valid_language_header(accept_language_header)
|
172
|
+
return true # FIXME: check with regex
|
173
|
+
end
|
174
|
+
|
175
|
+
def languages_in_accept_language(accept_language_header)
|
176
|
+
raw_langs = accept_language_header.split(',')
|
177
|
+
|
178
|
+
langs = raw_langs.map { |l| l.sub('q=', '')}.
|
179
|
+
map { |l| l.split(';') }
|
180
|
+
|
181
|
+
langs.each_with_index do |l, i|
|
182
|
+
l[0] = LanguageTag.parse(l[0])
|
183
|
+
l[1] = (l[1] || 1).to_f
|
184
|
+
|
185
|
+
sorting_epsilon = (langs.size - i).to_f / 100
|
186
|
+
l[1] += sorting_epsilon # keep the original order when sorting
|
187
|
+
end
|
188
|
+
|
189
|
+
return langs
|
190
|
+
end
|
191
|
+
|
192
|
+
def regex_for_languages_in_path
|
193
|
+
all_languages = @avail_languages.keys.map(&:alpha3)
|
194
|
+
|
195
|
+
preamble = "/"
|
196
|
+
body = "(" + all_languages.join("|") + ")"
|
197
|
+
trail = "/?$"
|
198
|
+
|
199
|
+
return Regexp.new(preamble + body + trail)
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
def self.accept_language_format
|
204
|
+
lang = '[-_a-zA-Z]+'
|
205
|
+
qvalue = '(; ?q=[01]+(\.[0-9]{1,3})?)'
|
206
|
+
|
207
|
+
return Regexp.new("\\A#{lang}#{qvalue}?(, ?#{lang}#{qvalue}?)*\\Z")
|
208
|
+
end
|
209
|
+
|
210
|
+
HEADER_FORMAT = self.accept_language_format
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# This is free software released into the public domain (CC0 license).
|
2
|
+
#
|
3
|
+
# See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
|
4
|
+
# for more details.
|
5
|
+
|
6
|
+
|
7
|
+
# BCP 47
|
8
|
+
|
9
|
+
class LanguageTag
|
10
|
+
VERSION = "5646.0.1"
|
11
|
+
|
12
|
+
A3_TO_A2 = {
|
13
|
+
'ara' => 'ar',
|
14
|
+
'deu' => 'de',
|
15
|
+
'eng' => 'en',
|
16
|
+
'fra' => 'fr',
|
17
|
+
'ita' => 'it',
|
18
|
+
}
|
19
|
+
|
20
|
+
def self.parse(raw_code)
|
21
|
+
iso_code = raw_code.split('-').flatten.first
|
22
|
+
|
23
|
+
if iso_code.nil?
|
24
|
+
raise "Unparseable language tag"
|
25
|
+
end
|
26
|
+
|
27
|
+
if !(A3_TO_A2.keys + A3_TO_A2.values).include?(iso_code)
|
28
|
+
raise "Unknown language tag #{iso_code}" #FIXME
|
29
|
+
end
|
30
|
+
|
31
|
+
return LanguageTag.new(iso_code)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(iso_code, extlang = nil, script = nil, region = nil, variant = [], extension = [], privateuse = nil)
|
35
|
+
case iso_code.length
|
36
|
+
when 3
|
37
|
+
@alpha3 = iso_code
|
38
|
+
when 2
|
39
|
+
@alpha2 = iso_code
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def alpha2
|
44
|
+
@alpha2 ||= a3_to_a2(@alpha3)
|
45
|
+
end
|
46
|
+
|
47
|
+
def alpha3
|
48
|
+
@alpha3 ||= a2_to_a3(@alpha2)
|
49
|
+
end
|
50
|
+
|
51
|
+
def a3_to_a2(alpha3)
|
52
|
+
return A3_TO_A2[alpha3]
|
53
|
+
end
|
54
|
+
|
55
|
+
def a2_to_a3(alpha2)
|
56
|
+
return A3_TO_A2.invert[alpha2]
|
57
|
+
end
|
58
|
+
|
59
|
+
def complete
|
60
|
+
alpha3
|
61
|
+
end
|
62
|
+
|
63
|
+
def ==(other_code)
|
64
|
+
return self.complete == LanguageTag.new(other_code).complete
|
65
|
+
end
|
66
|
+
|
67
|
+
def hash
|
68
|
+
self.complete.hash
|
69
|
+
end
|
70
|
+
|
71
|
+
def eql?(other)
|
72
|
+
if self.equal?(other)
|
73
|
+
return true
|
74
|
+
elsif self.class != other.class
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
|
78
|
+
return self.alpha3 == other.alpha3
|
79
|
+
end
|
80
|
+
|
81
|
+
def inspect
|
82
|
+
return "#<LocaleCode '#{complete}'>"
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# This is free software released into the public domain (CC0 license).
|
2
|
+
#
|
3
|
+
# See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
|
4
|
+
# for more details.
|
5
|
+
|
6
|
+
|
7
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
8
|
+
|
9
|
+
describe Rack::I18nBestLangs do
|
10
|
+
AVAIL_LANGUAGES = ['ita', 'fra', 'eng', 'ara']
|
11
|
+
|
12
|
+
def app(*opts)
|
13
|
+
builder = Rack::Builder.new do
|
14
|
+
use Rack::Lint
|
15
|
+
use Rack::I18nBestLangs, *opts
|
16
|
+
use Rack::Lint
|
17
|
+
|
18
|
+
run lambda { |env| [200, {"Content-Type" => "text/plain"}, [""]] }
|
19
|
+
end
|
20
|
+
|
21
|
+
return builder.to_app
|
22
|
+
end
|
23
|
+
|
24
|
+
def request_with(path, env_opts = {}, *i18n_opts)
|
25
|
+
if i18n_opts.empty?
|
26
|
+
extra_opts = {}
|
27
|
+
|
28
|
+
i18n_opts << AVAIL_LANGUAGES # known_languages
|
29
|
+
i18n_opts << extra_opts
|
30
|
+
end
|
31
|
+
|
32
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app(*i18n_opts)))
|
33
|
+
session.request(path, env_opts)
|
34
|
+
|
35
|
+
return session.last_request
|
36
|
+
end
|
37
|
+
|
38
|
+
def http_langs(*langs)
|
39
|
+
{ 'HTTP_ACCEPT_LANGUAGE' => langs.flatten.join(', ') }
|
40
|
+
end
|
41
|
+
|
42
|
+
def cookie_langs(*langs)
|
43
|
+
{ 'HTTP_COOKIE' => Rack::I18nBestLangs::RACK_VARIABLE + "=" + langs.flatten.join(',') }
|
44
|
+
end
|
45
|
+
|
46
|
+
def aliases_mapping(aliases, default)
|
47
|
+
mapping = Rack::I18nRoutes::AliasMapping.new(aliases, :default => default)
|
48
|
+
|
49
|
+
return { :path_mapping_fn => mapping }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with no external information" do
|
53
|
+
it "suggests exactly the list of languages" do
|
54
|
+
env = request_with('/').env
|
55
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
56
|
+
|
57
|
+
languages.should be_an Array
|
58
|
+
languages.should == AVAIL_LANGUAGES
|
59
|
+
end
|
60
|
+
|
61
|
+
it "is not confused by paths that look like languages" do
|
62
|
+
env = request_with('/francesca').env
|
63
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
64
|
+
|
65
|
+
languages.should == AVAIL_LANGUAGES
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "with language in path" do
|
70
|
+
it "places that language as best language when available" do
|
71
|
+
env = request_with('/fra/').env
|
72
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
73
|
+
|
74
|
+
languages.first.should eq('fra')
|
75
|
+
languages.should include(*AVAIL_LANGUAGES)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "ignores that language when not available" do
|
79
|
+
env = request_with('/lat/').env
|
80
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
81
|
+
|
82
|
+
languages.should == AVAIL_LANGUAGES
|
83
|
+
end
|
84
|
+
|
85
|
+
it "selects the first path component" do
|
86
|
+
env = request_with('http://italia.example.org/foo/fra').env
|
87
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
88
|
+
|
89
|
+
languages.first.should eq('fra')
|
90
|
+
languages.should include(*AVAIL_LANGUAGES)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "removes the language from the path" do
|
94
|
+
env = request_with('/foo/fra').env
|
95
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
96
|
+
|
97
|
+
languages.first.should eq('fra')
|
98
|
+
env['PATH_INFO'].should eq('/foo')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "with language in headers" do
|
103
|
+
it "places that language as best language when available" do
|
104
|
+
env = request_with('/hello', http_langs('fr-FR')).env
|
105
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
106
|
+
|
107
|
+
languages.first.should == 'fra'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context "with language in cookie" do
|
112
|
+
it "places that language as best language when available" do
|
113
|
+
env = request_with('/hello', cookie_langs('eng')).env
|
114
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
115
|
+
|
116
|
+
languages.first.should == 'eng'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "with language implied in path and AliasMapper" do
|
121
|
+
let(:aliases) { Hash['house' => { 'ita' => 'casa', 'fra' => 'maison' }] }
|
122
|
+
let(:default_lang) { 'unk' }
|
123
|
+
|
124
|
+
it "places the most common non-default language as best language" do
|
125
|
+
env = request_with('/maison', {}, AVAIL_LANGUAGES, aliases_mapping(aliases, default_lang)).env
|
126
|
+
languages = env[Rack::I18nBestLangs::RACK_VARIABLE]
|
127
|
+
|
128
|
+
languages.first.should == 'fra'
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# This is free software released into the public domain (CC0 license).
|
2
|
+
#
|
3
|
+
# See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
|
4
|
+
# for more details.
|
5
|
+
|
6
|
+
|
7
|
+
LIB_DIR = File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib]))
|
8
|
+
$LOAD_PATH.unshift(LIB_DIR) unless $LOAD_PATH.include?(LIB_DIR)
|
9
|
+
|
10
|
+
require 'rack/i18n_best_langs'
|
11
|
+
require 'rack/i18n_routes'
|
12
|
+
require 'rack/test'
|
13
|
+
|
14
|
+
include Rack::Test::Methods
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
end
|
18
|
+
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-i18n_best_langs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: "0.2"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Gioele Barabucci
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-08-07 00:00:00 Z
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: rack
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 5
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 4
|
31
|
+
- 1
|
32
|
+
version: 1.4.1
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rack-test
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 5
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
- 6
|
47
|
+
- 1
|
48
|
+
version: 0.6.1
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rack-i18n_routes
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 959707343
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
- 3
|
63
|
+
- dev
|
64
|
+
version: 0.3.dev
|
65
|
+
type: :development
|
66
|
+
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: bones-rspec
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 13
|
76
|
+
segments:
|
77
|
+
- 2
|
78
|
+
- 0
|
79
|
+
- 1
|
80
|
+
version: 2.0.1
|
81
|
+
type: :development
|
82
|
+
version_requirements: *id004
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bones
|
85
|
+
prerelease: false
|
86
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 39
|
92
|
+
segments:
|
93
|
+
- 3
|
94
|
+
- 8
|
95
|
+
- 0
|
96
|
+
version: 3.8.0
|
97
|
+
type: :development
|
98
|
+
version_requirements: *id005
|
99
|
+
description: |-
|
100
|
+
rack-i18n_best_langs is a Rack middleware component that takes care of
|
101
|
+
understanding what are the best languages for a site visitor.
|
102
|
+
email: gioele@svario.it
|
103
|
+
executables: []
|
104
|
+
|
105
|
+
extensions: []
|
106
|
+
|
107
|
+
extra_rdoc_files: []
|
108
|
+
|
109
|
+
files:
|
110
|
+
- .yardopts
|
111
|
+
- COPYING
|
112
|
+
- README.md
|
113
|
+
- Rakefile
|
114
|
+
- lib/rack/i18n_best_langs.rb
|
115
|
+
- lib/rack/language_tag.rb
|
116
|
+
- spec/i18n-best-langs_spec.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
homepage: https://github.com/gioele/rack-i18n_best_langs
|
119
|
+
licenses: []
|
120
|
+
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options:
|
123
|
+
- --main
|
124
|
+
- README.md
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
hash: 3
|
133
|
+
segments:
|
134
|
+
- 0
|
135
|
+
version: "0"
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
hash: 3
|
142
|
+
segments:
|
143
|
+
- 0
|
144
|
+
version: "0"
|
145
|
+
requirements: []
|
146
|
+
|
147
|
+
rubyforge_project: rack-i18n_best_langs
|
148
|
+
rubygems_version: 1.8.24
|
149
|
+
signing_key:
|
150
|
+
specification_version: 3
|
151
|
+
summary: rack-i18n_best_langs is a Rack middleware component that takes care of understanding what are the best languages for a site visitor.
|
152
|
+
test_files: []
|
153
|
+
|