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 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: []