accept_language 2.0.6 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|