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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23f2f41d72b0b3ba8c16bfb544a85ac5d35fc509f1f057fee5d64c7756215dff
4
- data.tar.gz: a32c6e726b1bb521c7478b11bde767e7c8990320892efa928e39814c97334931
3
+ metadata.gz: 0afa534a141f111edb4f47449bccb2b44795abd11512319bee75909f672133ab
4
+ data.tar.gz: e90f24a3a8104eb8fff13ee15ce7660705e31565769c95e62d4fb4577ff1c816
5
5
  SHA512:
6
- metadata.gz: 6b161891fe7c1014dbe547e4ad932466f0e22741990ca6fbe43f9a559b20e789fd3323ea78211a2c7feb5e5eda8efca6e30c902edc4505d66ccdc89b9158fd11
7
- data.tar.gz: 2c25818bea47bfc094315b06a116d96583c3aeb3fb36e5bd309b3d74bbea679c0153e5a99dabe4af33785d2b9004644f7c680fa831e4bae7eea9e18a79baee22
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 is primarily designed to assist web servers in serving multilingual content based on user preferences expressed in the `Accept-Language` header. This library finds the best matching language from the available languages your application supports and the languages the user prefers.
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
- Below are some examples of how you might use the library:
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
- # The user prefers Danish, then British English, and finally any kind of English.
57
- # Since your application supports English and Danish, it selects Danish as it's the user's first choice.
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
- # The user prefers Danish, then English, and finally Uyghur. Your application supports British English and Chinese Uyghur.
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
- # The user prefers Danish, then British English, and finally any kind of English. Your application only supports Japanese.
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
- # The user only accepts Swiss French, but your application only supports French. Since Swiss French and French are not the same, it returns nil.
69
- AcceptLanguage.parse("fr-CH").match(:fr) # => nil
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
- # The user prefers German, then any language except French. Your application supports French.
72
- # Even though the user specified a wildcard, they explicitly excluded French. Therefore, it returns nil.
73
- AcceptLanguage.parse("de, zh;q=0.4, *;q=0.5, fr;q=0").match(:fr) # => nil
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
- # The user prefers Uyghur (in Latin script, as used in Uzbekistan). Your application supports this exact variant of Uyghur.
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
- # The user doesn't mind what language they get, but they'd prefer not to have English. Your application supports English.
80
- # Even though the user specified a wildcard, they explicitly excluded English. Therefore, it returns nil.
81
- AcceptLanguage.parse("*, en;q=0").match("en") # => nil
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
- These examples show the flexibility and power of `Accept Language`. By giving your application a deep understanding of the user's language preferences, `Accept Language` can significantly improve user satisfaction and engagement with your application.
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
- # A utility class that provides functionality to match the Accept-Language header value
5
- # against the languages supported by your application. This helps in identifying the most
6
- # suitable languages to present to the user based on their preferences.
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
- # Matches the user's preferred languages against the available languages of your application.
38
- # It prioritizes higher quality values and returns the most suitable match.
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 An array representing the languages available in your application.
41
- #
42
- # @example When Uyghur, Kazakh, Russian and English languages are available.
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
- available_langtags = drop_unacceptable(*available_langtags)
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
- if wildcard?(preferred_tag)
51
- langtag = any_other_langtag(*available_langtags)
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
- private
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? do |excluded_langtag|
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
- # Parser is a utility class responsible for parsing Accept-Language header fields.
7
- # It processes the field to extract language tags and their respective quality values.
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 = BigDecimal("1")
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
- # Uses the Matcher class to find the best language match from the list of available languages.
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
- # @example When Uyghur, Kazakh, Russian and English languages are available.
34
- # match(:ug, :kk, :ru, :en)
35
- #
36
- # @return [String, Symbol, nil] The language tag that best matches the parsed languages from the Accept-Language header, or nil if no match found.
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 if tag.nil?
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
- quality = quality.nil? ? DEFAULT_QUALITY : BigDecimal(quality)
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
 
@@ -1,13 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This module provides a tiny library for parsing the Accept-Language header as specified in RFC 2616.
4
- # It transforms the Accept-Language header field into a language range, providing a flexible way to determine
5
- # user's language preferences and match them with the available languages in your application.
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.6
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-01-12 00:00:00.000000000 Z
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.22
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: []