accept_language 2.0.6 → 2.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +82 -22
- data/lib/accept_language/matcher.rb +36 -28
- data/lib/accept_language/parser.rb +31 -13
- data/lib/accept_language.rb +16 -3
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0afa534a141f111edb4f47449bccb2b44795abd11512319bee75909f672133ab
|
4
|
+
data.tar.gz: e90f24a3a8104eb8fff13ee15ce7660705e31565769c95e62d4fb4577ff1c816
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d098319c113b5ed61b7b57a57ce78cde8b799d9256240dc076de8626d5021ad4840f96b23b94bcba1d64dc7930e66514da17d4d55d8107c417a9c102a99925b
|
7
|
+
data.tar.gz: c4695ea31539c0c94713b569e424fd2901a85a4ac67a448ef1c9cd200333fdaa1ab4bfe7429d1b7155fb2c4f4d2ffcf4218d2fee2060f9ffb6b629accc50a56e
|
data/README.md
CHANGED
@@ -48,40 +48,99 @@ gem install accept_language
|
|
48
48
|
|
49
49
|
## Usage
|
50
50
|
|
51
|
-
`Accept Language` library
|
51
|
+
The `Accept Language` library helps web applications serve content in the user's preferred language by parsing the `Accept-Language` HTTP header. This header indicates the user's language preferences and their priority order.
|
52
52
|
|
53
|
-
|
53
|
+
### Basic Syntax
|
54
|
+
|
55
|
+
The library has two main methods:
|
56
|
+
|
57
|
+
- `AcceptLanguage.parse(header)`: Parses the Accept-Language header
|
58
|
+
- `match(*available_languages)`: Matches against the languages your application supports
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
AcceptLanguage.parse("fr-CH, fr;q=0.9").match(:fr, :"fr-CH") # => :"fr-CH"
|
62
|
+
```
|
63
|
+
|
64
|
+
### Understanding Language Preferences
|
65
|
+
|
66
|
+
#### Simple Language Matching
|
54
67
|
|
55
68
|
```ruby
|
56
|
-
#
|
57
|
-
#
|
69
|
+
# Header: "da" (Danish is the preferred language)
|
70
|
+
# Available: :en and :da
|
71
|
+
AcceptLanguage.parse("da").match(:en, :da) # => :da
|
72
|
+
|
73
|
+
# No match available - returns nil
|
74
|
+
AcceptLanguage.parse("da").match(:fr, :en) # => nil
|
75
|
+
```
|
76
|
+
|
77
|
+
#### Quality Values (q-values)
|
78
|
+
|
79
|
+
Q-values range from 0 to 1 and indicate preference order:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# Header: "da, en-GB;q=0.8, en;q=0.7"
|
83
|
+
# Means:
|
84
|
+
# - Danish (da): q=1.0 (highest priority)
|
85
|
+
# - British English (en-GB): q=0.8 (second choice)
|
86
|
+
# - Generic English (en): q=0.7 (third choice)
|
58
87
|
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da) # => :da
|
88
|
+
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :"en-GB") # => :"en-GB"
|
89
|
+
```
|
59
90
|
|
60
|
-
|
61
|
-
# Here, the library will return Chinese Uyghur because it's the highest ranked language in the user's list that your application supports.
|
62
|
-
AcceptLanguage.parse("da, en;q=0.8, ug;q=0.9").match("en-GB", "ug-CN") # => "ug-CN"
|
91
|
+
#### Language Variants
|
63
92
|
|
64
|
-
|
65
|
-
# Since none of the user's preferred languages are supported, it returns nil.
|
66
|
-
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:ja) # => nil
|
93
|
+
The library handles specific language variants (regional or script variations):
|
67
94
|
|
68
|
-
|
69
|
-
|
95
|
+
```ruby
|
96
|
+
# Specific variants must match exactly
|
97
|
+
AcceptLanguage.parse("fr-CH").match(:fr) # => nil (Swiss French ≠ Generic French)
|
98
|
+
|
99
|
+
# But generic variants can match specific ones
|
100
|
+
AcceptLanguage.parse("fr").match(:"fr-CH") # => :"fr-CH"
|
101
|
+
|
102
|
+
# Script variants are also supported
|
103
|
+
AcceptLanguage.parse("uz-Latn-UZ").match("uz-Latn-UZ") # => "uz-Latn-UZ"
|
104
|
+
```
|
105
|
+
|
106
|
+
#### Wildcards and Exclusions
|
107
|
+
|
108
|
+
The `*` wildcard matches any language, and `q=0` excludes languages:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# Accept any language but prefer German
|
112
|
+
AcceptLanguage.parse("de-DE, *;q=0.5").match(:fr) # => :fr (matched by wildcard)
|
70
113
|
|
71
|
-
#
|
72
|
-
|
73
|
-
AcceptLanguage.parse("
|
114
|
+
# Accept any language EXCEPT English
|
115
|
+
AcceptLanguage.parse("*, en;q=0").match(:en) # => nil (explicitly excluded)
|
116
|
+
AcceptLanguage.parse("*, en;q=0").match(:fr) # => :fr (matched by wildcard)
|
117
|
+
```
|
74
118
|
|
75
|
-
|
76
|
-
# Since the user's first choice matches a language your application supports, it returns that language.
|
77
|
-
AcceptLanguage.parse("uz-latn-uz").match("uz-Latn-UZ") # => "uz-Latn-UZ"
|
119
|
+
#### Complex Example
|
78
120
|
|
79
|
-
|
80
|
-
#
|
81
|
-
|
121
|
+
```ruby
|
122
|
+
# Header: "de-LU, fr;q=0.9, en;q=0.7, *;q=0.5"
|
123
|
+
# Means:
|
124
|
+
# - Luxembourg German: q=1.0 (highest priority)
|
125
|
+
# - French: q=0.9 (second choice)
|
126
|
+
# - English: q=0.7 (third choice)
|
127
|
+
# - Any other language: q=0.5 (lowest priority)
|
128
|
+
header = "de-LU, fr;q=0.9, en;q=0.7, *;q=0.5"
|
129
|
+
parser = AcceptLanguage.parse(header)
|
130
|
+
|
131
|
+
parser.match(:de, :"de-LU") # => :"de-LU" (exact match)
|
132
|
+
parser.match(:fr, :en) # => :fr (higher q-value)
|
133
|
+
parser.match(:es, :it) # => :es (matched by wildcard)
|
82
134
|
```
|
83
135
|
|
84
|
-
|
136
|
+
### Case Sensitivity
|
137
|
+
|
138
|
+
The matching is case-insensitive but preserves the case of the returned value:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
AcceptLanguage.parse("en-GB").match("en-gb") # => "en-gb"
|
142
|
+
AcceptLanguage.parse("en-gb").match("en-GB") # => "en-GB"
|
143
|
+
```
|
85
144
|
|
86
145
|
### Rails integration example
|
87
146
|
|
@@ -95,6 +154,7 @@ class ApplicationController < ActionController::Base
|
|
95
154
|
end
|
96
155
|
|
97
156
|
def best_locale_from_request
|
157
|
+
# HTTP_ACCEPT_LANGUAGE is the standardized key for the Accept-Language header in Rack/Rails
|
98
158
|
return I18n.default_locale unless request.headers.key?("HTTP_ACCEPT_LANGUAGE")
|
99
159
|
|
100
160
|
string = request.headers.fetch("HTTP_ACCEPT_LANGUAGE")
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AcceptLanguage
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
4
|
+
# Matches Accept-Language header values against application-supported languages to determine
|
5
|
+
# the optimal language choice. Handles quality values, wildcards, and language tag matching
|
6
|
+
# according to RFC 2616 specifications.
|
7
7
|
#
|
8
8
|
# @example
|
9
9
|
# Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:ug, :kk, :ru, :en) # => :en
|
@@ -34,41 +34,51 @@ module AcceptLanguage
|
|
34
34
|
@preferred_langtags = langtags.compact.reverse
|
35
35
|
end
|
36
36
|
|
37
|
-
#
|
38
|
-
#
|
37
|
+
# Finds the optimal language match by comparing user preferences against available languages.
|
38
|
+
# Handles priorities based on:
|
39
|
+
# 1. Explicit quality values (q-values)
|
40
|
+
# 2. Language tag specificity (exact matches preferred over partial matches)
|
41
|
+
# 3. Order of preference in the original Accept-Language header
|
39
42
|
#
|
40
|
-
# @param [Array<String, Symbol>] available_langtags
|
41
|
-
#
|
42
|
-
# @
|
43
|
-
# call(:ug, :kk, :ru, :en)
|
44
|
-
#
|
45
|
-
# @return [String, Symbol, nil] The language that best matches the user's preferences, or nil if there is no match.
|
43
|
+
# @param [Array<String, Symbol>] available_langtags Languages supported by your application
|
44
|
+
# @return [String, Symbol, nil] Best matching language or nil if no acceptable match found
|
45
|
+
# @raise [ArgumentError] If any language tag is nil
|
46
46
|
def call(*available_langtags)
|
47
|
-
|
47
|
+
raise ::ArgumentError, "Language tags cannot be nil" if available_langtags.any?(&:nil?)
|
48
|
+
|
49
|
+
filtered_tags = drop_unacceptable(*available_langtags)
|
50
|
+
return nil if filtered_tags.empty?
|
51
|
+
|
52
|
+
find_best_match(filtered_tags)
|
53
|
+
end
|
48
54
|
|
55
|
+
private
|
56
|
+
|
57
|
+
def find_best_match(available_langtags)
|
49
58
|
preferred_langtags.each do |preferred_tag|
|
50
|
-
|
51
|
-
|
52
|
-
return langtag unless langtag.nil?
|
53
|
-
else
|
54
|
-
available_langtags.each do |available_langtag|
|
55
|
-
return available_langtag if available_langtag.match?(/\A#{preferred_tag}/i)
|
56
|
-
end
|
57
|
-
end
|
59
|
+
match = match_langtag(preferred_tag, available_langtags)
|
60
|
+
return match if match
|
58
61
|
end
|
59
62
|
|
60
63
|
nil
|
61
64
|
end
|
62
65
|
|
63
|
-
|
66
|
+
def match_langtag(preferred_tag, available_langtags)
|
67
|
+
if wildcard?(preferred_tag)
|
68
|
+
any_other_langtag(*available_langtags)
|
69
|
+
else
|
70
|
+
find_matching_tag(preferred_tag, available_langtags)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_matching_tag(preferred_tag, available_langtags)
|
75
|
+
available_langtags.find { |tag| tag.match?(/\A#{preferred_tag}/i) }
|
76
|
+
end
|
64
77
|
|
65
78
|
def any_other_langtag(*available_langtags)
|
66
79
|
available_langtags.find do |available_langtag|
|
67
80
|
langtags = preferred_langtags - [WILDCARD]
|
68
|
-
|
69
|
-
langtags.none? do |langtag|
|
70
|
-
available_langtag.match?(/\A#{langtag}/i)
|
71
|
-
end
|
81
|
+
langtags.none? { |tag| available_langtag.match?(/\A#{tag}/i) }
|
72
82
|
end
|
73
83
|
end
|
74
84
|
|
@@ -81,9 +91,7 @@ module AcceptLanguage
|
|
81
91
|
end
|
82
92
|
|
83
93
|
def unacceptable?(langtag)
|
84
|
-
excluded_langtags.any?
|
85
|
-
langtag.match?(/\A#{excluded_langtag}/i)
|
86
|
-
end
|
94
|
+
excluded_langtags.any? { |excluded_tag| langtag.match?(/\A#{excluded_tag}/i) }
|
87
95
|
end
|
88
96
|
|
89
97
|
def wildcard?(value)
|
@@ -3,8 +3,9 @@
|
|
3
3
|
require "bigdecimal"
|
4
4
|
|
5
5
|
module AcceptLanguage
|
6
|
-
#
|
7
|
-
#
|
6
|
+
# Parses Accept-Language header fields into structured data, extracting language tags
|
7
|
+
# and their quality values (q-values). Validates input according to RFC 2616 specifications
|
8
|
+
# and handles edge cases like malformed inputs and implicit quality values.
|
8
9
|
#
|
9
10
|
# @example
|
10
11
|
# Parser.new("da, en-GB;q=0.8, en;q=0.7")
|
@@ -12,11 +13,17 @@ module AcceptLanguage
|
|
12
13
|
#
|
13
14
|
# @see https://tools.ietf.org/html/rfc2616#section-14.4 for more information on Accept-Language header fields.
|
14
15
|
class Parser
|
15
|
-
DEFAULT_QUALITY =
|
16
|
+
DEFAULT_QUALITY = "1"
|
16
17
|
SEPARATOR = ","
|
17
18
|
SPACE = " "
|
18
19
|
SUFFIX = ";q="
|
19
20
|
|
21
|
+
# Validates q-values according to RFC 2616:
|
22
|
+
# - Must be between 0 and 1
|
23
|
+
# - Can have up to 3 decimal places
|
24
|
+
# - Allows both forms: .8 and 0.8
|
25
|
+
QVALUE_PATTERN = /\A(?:0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?|\.[0-9]{1,3})\z/
|
26
|
+
|
20
27
|
attr_reader :languages_range
|
21
28
|
|
22
29
|
# Initializes a new Parser instance by importing and processing the given Accept-Language header field.
|
@@ -26,14 +33,15 @@ module AcceptLanguage
|
|
26
33
|
@languages_range = import(field)
|
27
34
|
end
|
28
35
|
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# @param [Array<String, Symbol>] available_langtags An array of language tags that are available for matching.
|
36
|
+
# Finds the best matching language from available options based on user preferences.
|
37
|
+
# Considers quality values and language tag specificity (e.g., "en-US" vs "en").
|
32
38
|
#
|
33
|
-
# @
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
39
|
+
# @param [Array<String, Symbol>] available_langtags Languages supported by your application
|
40
|
+
# @return [String, Symbol, nil] Best matching language tag or nil if no match found
|
41
|
+
# @example Match against specific language options
|
42
|
+
# parser.match("en", "fr", "de") # => "en" if English is preferred
|
43
|
+
# @example Match with region-specific tags
|
44
|
+
# parser.match("en-US", "en-GB", "fr") # => "en-GB" if British English is preferred
|
37
45
|
def match(*available_langtags)
|
38
46
|
Matcher.new(**languages_range).call(*available_langtags)
|
39
47
|
end
|
@@ -50,12 +58,22 @@ module AcceptLanguage
|
|
50
58
|
def import(field)
|
51
59
|
"#{field}".delete(SPACE).split(SEPARATOR).inject({}) do |hash, lang|
|
52
60
|
tag, quality = lang.split(SUFFIX)
|
53
|
-
next hash
|
61
|
+
next hash unless valid_tag?(tag)
|
62
|
+
|
63
|
+
quality = DEFAULT_QUALITY if quality.nil?
|
64
|
+
next hash unless valid_quality?(quality)
|
54
65
|
|
55
|
-
|
56
|
-
hash.merge(tag => quality)
|
66
|
+
hash.merge(tag => BigDecimal(quality))
|
57
67
|
end
|
58
68
|
end
|
69
|
+
|
70
|
+
def valid_quality?(quality)
|
71
|
+
quality.match?(QVALUE_PATTERN)
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid_tag?(tag)
|
75
|
+
!tag.nil? && !tag.empty?
|
76
|
+
end
|
59
77
|
end
|
60
78
|
end
|
61
79
|
|
data/lib/accept_language.rb
CHANGED
@@ -1,13 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
# It
|
5
|
-
#
|
3
|
+
# AcceptLanguage is a lightweight library that parses Accept-Language HTTP headers (RFC 2616) to determine
|
4
|
+
# user language preferences. It converts raw header values into a structured format for matching against
|
5
|
+
# your application's supported languages.
|
6
6
|
#
|
7
7
|
# @example
|
8
8
|
# AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7")
|
9
9
|
# # => #<AcceptLanguage::Parser:0x00007 @languages_range={"da"=>1.0, "en-GB"=>0.8, "en"=>0.7}>
|
10
10
|
#
|
11
|
+
# @example Integration with Rails
|
12
|
+
# class ApplicationController < ActionController::Base
|
13
|
+
# before_action :set_locale
|
14
|
+
#
|
15
|
+
# private
|
16
|
+
#
|
17
|
+
# def set_locale
|
18
|
+
# header = request.env["HTTP_ACCEPT_LANGUAGE"]
|
19
|
+
# locale = AcceptLanguage.parse(header).match(*I18n.available_locales)
|
20
|
+
# I18n.locale = locale || I18n.default_locale
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
11
24
|
# @see https://tools.ietf.org/html/rfc2616#section-14.4
|
12
25
|
module AcceptLanguage
|
13
26
|
# Parses an Accept-Language header field value into a Parser object, which can then be used to match
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: accept_language
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Parses the Accept-Language header from an HTTP request and produces a
|
14
14
|
hash of languages and qualities.
|
@@ -27,7 +27,7 @@ licenses:
|
|
27
27
|
- MIT
|
28
28
|
metadata:
|
29
29
|
rubygems_mfa_required: 'true'
|
30
|
-
post_install_message:
|
30
|
+
post_install_message:
|
31
31
|
rdoc_options: []
|
32
32
|
require_paths:
|
33
33
|
- lib
|
@@ -42,8 +42,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: '0'
|
44
44
|
requirements: []
|
45
|
-
rubygems_version: 3.4.
|
46
|
-
signing_key:
|
45
|
+
rubygems_version: 3.4.10
|
46
|
+
signing_key:
|
47
47
|
specification_version: 4
|
48
48
|
summary: "Parser for Accept-Language request HTTP header \U0001F310"
|
49
49
|
test_files: []
|