accept_language 2.1.0 → 2.1.1

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: 7273e9328183e3dee11fd68a6598d82c67efbb8bab156d6d9b3424d9ed45dcca
4
- data.tar.gz: ec31e8a4ac07501f1c481e65452f362be2d7669d93eea626977f25c3aca88dc2
3
+ metadata.gz: c1d4f90fc40c062ac4f250c1c3b8ac8540796b232cf63b7d5c25b5e2e3c9124e
4
+ data.tar.gz: 150bb9def9e5f4799c432df6d6c364fedf88132b117d28e79a6ed44f467dcc70
5
5
  SHA512:
6
- metadata.gz: 36120af5b03b49ea9dce1d50e5c7915c62bf9b0fa09c1130516090e4e3c882b924035f963ad2dc559fda0e818633b6a3e77aba3e46b25533bc972f5dc23ca729
7
- data.tar.gz: d11855f60c7a4a35c8f675ea5ffc4b320db5a4b40cc95e57143b3dad3c188580830ae0052ac7e934f5ae4c5f2be582db47b4e85b9513b78b4ec1fc69a97ee850
6
+ metadata.gz: 537d924a23dc3c0fe8fb523556a6da653e16471b6b8ca94e0ee57f36fa1d6842103e380c3ea1e2dbd468b06a65a698ecdbf8923235f4eac0031a52801ba446f8
7
+ data.tar.gz: 8e48f57d2f0a4005483d29cf5503accc39eecffe19f78e8848dfa216422d1b2792f50cc5199c938674a483ef90ca20127fbfe452c0eebae9e30b36d7b8dc0bc2
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AcceptLanguage
2
2
 
3
- A lightweight, thread-safe Ruby library for parsing `Accept-Language` HTTP headers as defined in [RFC 2616](https://tools.ietf.org/html/rfc2616#section-14.4).
3
+ A lightweight, thread-safe Ruby library for parsing `Accept-Language` HTTP headers as defined in [RFC 2616](https://tools.ietf.org/html/rfc2616#section-14.4), with full support for [BCP 47](https://tools.ietf.org/html/bcp47) language tags.
4
4
 
5
5
  [![Version](https://img.shields.io/github/v/tag/cyril/accept_language.rb?label=Version&logo=github)](https://github.com/cyril/accept_language.rb/tags)
6
6
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/cyril/accept_language.rb/main)
@@ -69,6 +69,24 @@ AcceptLanguage.parse("en-GB").match("en-gb") # => "en-gb"
69
69
  AcceptLanguage.parse("en-gb").match("en-GB") # => "en-GB"
70
70
  ```
71
71
 
72
+ ### BCP 47 support
73
+
74
+ This library supports [BCP 47](https://tools.ietf.org/html/bcp47) language tags, including:
75
+
76
+ - **Script subtags**: `zh-Hans` (Simplified Chinese), `zh-Hant` (Traditional Chinese)
77
+ - **Region subtags**: `en-US`, `pt-BR`
78
+ - **Variant subtags**: `sl-nedis` (Slovenian Nadiza dialect), `de-1996` (German orthography reform)
79
+
80
+ ```ruby
81
+ # Script variants
82
+ AcceptLanguage.parse("zh-Hans").match(:"zh-Hans-CN", :"zh-Hant-TW")
83
+ # => :"zh-Hans-CN"
84
+
85
+ # Orthography variants (numeric subtags)
86
+ AcceptLanguage.parse("de-1996, de;q=0.9").match(:"de-CH-1996", :"de-CH")
87
+ # => :"de-CH-1996"
88
+ ```
89
+
72
90
  ## Rails integration
73
91
 
74
92
  ```ruby
@@ -21,6 +21,10 @@ module AcceptLanguage
21
21
 
22
22
  languages_range.select do |langtag, quality|
23
23
  if quality.zero?
24
+ # Exclude specific language tags, but NOT the wildcard.
25
+ # When "*;q=0" is specified, all non-listed languages become
26
+ # unacceptable implicitly (they won't match any preferred_langtags).
27
+ # Adding "*" to excluded_langtags would break prefix_match? logic.
24
28
  @excluded_langtags << langtag unless wildcard?(langtag)
25
29
  else
26
30
  level = (quality * 1_000).to_i
@@ -61,37 +65,43 @@ module AcceptLanguage
61
65
  end
62
66
 
63
67
  def find_matching_tag(preferred_tag, available_langtags)
64
- pattern = /\A#{::Regexp.escape(preferred_tag)}/i
65
- available_langtags.find { |tag| tag.match?(pattern) }
68
+ available_langtags.find { |tag| prefix_match?(preferred_tag, String(tag.downcase)) }
66
69
  end
67
70
 
68
71
  def any_other_langtag(*available_langtags)
72
+ langtags = preferred_langtags - [WILDCARD]
73
+
69
74
  available_langtags.find do |available_langtag|
70
- langtags = preferred_langtags - [WILDCARD]
71
- langtags.none? do |tag|
72
- pattern = /\A#{::Regexp.escape(tag)}/i
73
- available_langtag.match?(pattern)
74
- end
75
+ available_downcased = available_langtag.downcase
76
+ langtags.none? { |tag| prefix_match?(tag, String(available_downcased)) }
75
77
  end
76
78
  end
77
79
 
78
80
  def drop_unacceptable(*available_langtags)
79
- available_langtags.inject(::Set[]) do |langtags, available_langtag|
80
- next langtags if unacceptable?(available_langtag)
81
-
82
- langtags + ::Set[available_langtag]
81
+ available_langtags.each_with_object(::Set[]) do |available_langtag, langtags|
82
+ langtags << available_langtag unless unacceptable?(available_langtag)
83
83
  end
84
84
  end
85
85
 
86
86
  def unacceptable?(langtag)
87
- excluded_langtags.any? do |excluded_tag|
88
- pattern = /\A#{::Regexp.escape(excluded_tag)}/i
89
- langtag.match?(pattern)
90
- end
87
+ langtag_downcased = langtag.downcase
88
+ excluded_langtags.any? { |excluded_tag| prefix_match?(excluded_tag, String(langtag_downcased)) }
91
89
  end
92
90
 
93
91
  def wildcard?(value)
94
92
  value.eql?(WILDCARD)
95
93
  end
94
+
95
+ # Implements RFC 2616 Section 14.4 prefix matching rule:
96
+ # "A language-range matches a language-tag if it exactly equals the tag,
97
+ # or if it exactly equals a prefix of the tag such that the first tag
98
+ # character following the prefix is '-'."
99
+ #
100
+ # @param prefix [String] The language-range to match (downcased)
101
+ # @param tag [String] The language-tag to test (downcased)
102
+ # @return [Boolean] true if prefix matches tag per RFC 2616 rules
103
+ def prefix_match?(prefix, tag)
104
+ tag == prefix || tag.start_with?("#{prefix}-")
105
+ end
96
106
  end
97
107
  end
@@ -22,8 +22,21 @@ module AcceptLanguage
22
22
  # @api private
23
23
  SUFFIX = ";q="
24
24
  # @api private
25
- QVALUE_PATTERN = /\A(?:0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?|\.[0-9]{1,3})\z/
25
+ # RFC 2616 Section 3.9 qvalue syntax:
26
+ # qvalue = ( "0" [ "." 0*3DIGIT ] ) | ( "1" [ "." 0*3("0") ] )
27
+ QVALUE_PATTERN = /\A(?:0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?)\z/
26
28
  # @api private
29
+ # Language tag pattern supporting BCP 47 (RFC 5646) alphanumeric subtags.
30
+ #
31
+ # RFC 2616 Section 3.10 references RFC 1766, which only allowed ALPHA in subtags.
32
+ # However, BCP 47 (the current standard) permits alphanumeric subtags:
33
+ # subtag = 1*8alphanum
34
+ # alphanum = ALPHA / DIGIT
35
+ #
36
+ # Examples of valid BCP 47 tags with numeric subtags:
37
+ # - "de-CH-1996" (German, Switzerland, orthography variant 1996)
38
+ # - "sl-IT-nedis" (Slovenian, Italy, Nadiza dialect)
39
+ # - "zh-Hans-CN" (Chinese, Simplified script, China)
27
40
  LANGTAG_PATTERN = /\A(?:\*|[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*)\z/
28
41
 
29
42
  # @api private
@@ -54,7 +67,7 @@ module AcceptLanguage
54
67
  private
55
68
 
56
69
  def import(field)
57
- "#{field}".delete(SPACE).split(SEPARATOR).inject({}) do |hash, lang|
70
+ "#{field}".downcase.delete(SPACE).split(SEPARATOR).inject({}) do |hash, lang|
58
71
  tag, quality = lang.split(SUFFIX)
59
72
  next hash unless valid_tag?(tag)
60
73
 
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.1.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-15 00:00:00.000000000 Z
11
+ date: 2026-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal