accept_language 2.1.0 → 2.2.0

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.
@@ -1,27 +1,267 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # AcceptLanguage is a lightweight library for parsing Accept-Language HTTP headers
4
- # as defined in RFC 2616. It determines user language preferences and matches them
5
- # against your application's supported languages.
3
+ # = Accept-Language Header Parser
6
4
  #
7
- # @example Basic usage
5
+ # AcceptLanguage is a lightweight, thread-safe Ruby library for parsing the
6
+ # +Accept-Language+ HTTP header field as defined in RFC 2616 Section 14.4,
7
+ # with full support for BCP 47 (RFC 5646) language tags.
8
+ #
9
+ # == Purpose
10
+ #
11
+ # The +Accept-Language+ request-header field is sent by user agents to indicate
12
+ # the set of natural languages that are preferred as a response to the request.
13
+ # This library parses that header and matches the user's language preferences
14
+ # against your application's available languages, respecting quality values,
15
+ # wildcards, exclusions, and prefix matching rules defined by the HTTP/1.1
16
+ # specification.
17
+ #
18
+ # == Standards Compliance
19
+ #
20
+ # This implementation conforms to:
21
+ #
22
+ # - {RFC 2616 Section 14.4}[https://tools.ietf.org/html/rfc2616#section-14.4] -
23
+ # Accept-Language header field definition
24
+ # - {RFC 2616 Section 3.9}[https://tools.ietf.org/html/rfc2616#section-3.9] -
25
+ # Quality values (qvalues) syntax
26
+ # - {RFC 2616 Section 3.10}[https://tools.ietf.org/html/rfc2616#section-3.10] -
27
+ # Language tags reference
28
+ # - {BCP 47 / RFC 5646}[https://tools.ietf.org/html/bcp47] -
29
+ # Tags for Identifying Languages (modern standard for language tags)
30
+ #
31
+ # == Basic Usage
32
+ #
33
+ # require "accept_language"
34
+ #
35
+ # # Parse an Accept-Language header and find the best match
8
36
  # AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da)
9
37
  # # => :da
10
38
  #
11
- # @example With regional variants
39
+ # # With regional variants
12
40
  # AcceptLanguage.parse("fr-CH, fr;q=0.9").match(:fr, :"fr-CH")
13
41
  # # => :"fr-CH"
14
42
  #
15
- # @see https://tools.ietf.org/html/rfc2616#section-14.4
43
+ # # When no match is found
44
+ # AcceptLanguage.parse("ja, zh;q=0.9").match(:en, :fr)
45
+ # # => nil
46
+ #
47
+ # == Quality Values
48
+ #
49
+ # Quality values (q-values) express relative preference, ranging from +0+
50
+ # (explicitly unacceptable) to +1+ (most preferred). When omitted, the
51
+ # default quality value is +1+.
52
+ #
53
+ # Per RFC 2616 Section 3.9, valid q-values have at most three decimal places.
54
+ # Invalid q-values cause the associated language tag to be ignored.
55
+ #
56
+ # parser = AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7")
57
+ #
58
+ # parser.match(:en, :da) # => :da (q=1 beats q=0.8)
59
+ # parser.match(:en, :"en-GB") # => :"en-GB" (q=0.8 beats q=0.7)
60
+ #
61
+ # == Prefix Matching
62
+ #
63
+ # Per RFC 2616 Section 14.4, a language-range matches a language-tag if it
64
+ # exactly equals the tag, or if it exactly equals a prefix of the tag such
65
+ # that the first character following the prefix is a hyphen (+"-"+).
66
+ #
67
+ # # "zh" matches "zh-TW" (prefix match)
68
+ # AcceptLanguage.parse("zh").match(:"zh-TW")
69
+ # # => :"zh-TW"
70
+ #
71
+ # # "zh-TW" does NOT match "zh" (more specific cannot match less specific)
72
+ # AcceptLanguage.parse("zh-TW").match(:zh)
73
+ # # => nil
74
+ #
75
+ # # Prefix matching respects hyphen boundaries
76
+ # AcceptLanguage.parse("zh").match(:zhx)
77
+ # # => nil (zhx is a different language code, not a subtag of zh)
78
+ #
79
+ # == Wildcards
80
+ #
81
+ # The wildcard character +*+ matches any language not explicitly matched by
82
+ # another language-range in the header:
83
+ #
84
+ # # Wildcard matches any language
85
+ # AcceptLanguage.parse("de, *;q=0.5").match(:ja)
86
+ # # => :ja
87
+ #
88
+ # # Explicit matches take precedence over wildcard
89
+ # AcceptLanguage.parse("de, *;q=0.5").match(:de, :ja)
90
+ # # => :de
91
+ #
92
+ # == Exclusions
93
+ #
94
+ # A quality value of +0+ explicitly marks a language as unacceptable:
95
+ #
96
+ # # English is excluded despite wildcard
97
+ # AcceptLanguage.parse("*, en;q=0").match(:en)
98
+ # # => nil
99
+ #
100
+ # AcceptLanguage.parse("*, en;q=0").match(:ja)
101
+ # # => :ja
102
+ #
103
+ # # Exclusions apply to prefix matches
104
+ # AcceptLanguage.parse("*, en;q=0").match(:"en-GB")
105
+ # # => nil (en-GB is excluded via the "en" prefix)
106
+ #
107
+ # == Priority with Equal Quality Values
108
+ #
109
+ # When multiple languages share the same quality value, declaration order in
110
+ # the original header determines priority—the first declared language wins:
111
+ #
112
+ # AcceptLanguage.parse("en;q=0.8, fr;q=0.8").match(:en, :fr)
113
+ # # => :en (declared first)
114
+ #
115
+ # AcceptLanguage.parse("fr;q=0.8, en;q=0.8").match(:en, :fr)
116
+ # # => :fr (declared first)
117
+ #
118
+ # == Case Insensitivity
119
+ #
120
+ # Language tag matching is case-insensitive per RFC 2616, but the original
121
+ # case of available language tags provided to +match+ is preserved in the
122
+ # return value:
123
+ #
124
+ # AcceptLanguage.parse("EN-GB").match(:"en-gb")
125
+ # # => :"en-gb"
126
+ #
127
+ # AcceptLanguage.parse("en-gb").match(:"EN-GB")
128
+ # # => :"EN-GB"
129
+ #
130
+ # == BCP 47 Language Tags
131
+ #
132
+ # Full support for BCP 47 language tags including script subtags, region
133
+ # subtags, and variant subtags:
134
+ #
135
+ # # Script subtags (e.g., Hans for Simplified Chinese)
136
+ # AcceptLanguage.parse("zh-Hant").match(:"zh-Hant-TW", :"zh-Hans-CN")
137
+ # # => :"zh-Hant-TW"
138
+ #
139
+ # # Variant subtags (e.g., 1996 for German orthography reform)
140
+ # AcceptLanguage.parse("de-1996, de;q=0.9").match(:"de-CH-1996", :"de-CH")
141
+ # # => :"de-CH-1996"
142
+ #
143
+ # == Thread Safety
144
+ #
145
+ # All AcceptLanguage operations are thread-safe. Parser instances are
146
+ # immutable after initialization and can be safely shared between threads.
147
+ #
148
+ # == Rack Integration Example
149
+ #
150
+ # # config.ru
151
+ # class LocaleMiddleware
152
+ # def initialize(app, available_locales:, default_locale:)
153
+ # @app = app
154
+ # @available_locales = available_locales
155
+ # @default_locale = default_locale
156
+ # end
157
+ #
158
+ # def call(env)
159
+ # locale = detect_locale(env) || @default_locale
160
+ # env["rack.locale"] = locale
161
+ # @app.call(env)
162
+ # end
163
+ #
164
+ # private
165
+ #
166
+ # def detect_locale(env)
167
+ # header = env["HTTP_ACCEPT_LANGUAGE"]
168
+ # AcceptLanguage.parse(header).match(*@available_locales)
169
+ # end
170
+ # end
171
+ #
172
+ # == Rails Integration Example
173
+ #
174
+ # # app/controllers/application_controller.rb
175
+ # class ApplicationController < ActionController::Base
176
+ # before_action :best_locale_from_request!
177
+ #
178
+ # def best_locale_from_request!
179
+ # I18n.locale = best_locale_from_request
180
+ # end
181
+ #
182
+ # def best_locale_from_request
183
+ # # HTTP_ACCEPT_LANGUAGE is the standardized key for the Accept-Language header in Rack/Rails
184
+ # return I18n.default_locale unless request.headers.key?("HTTP_ACCEPT_LANGUAGE")
185
+ #
186
+ # string = request.headers.fetch("HTTP_ACCEPT_LANGUAGE")
187
+ # locale = AcceptLanguage.parse(string).match(*I18n.available_locales)
188
+ #
189
+ # # If the server cannot serve any matching language,
190
+ # # it can theoretically send back a 406 (Not Acceptable) error code.
191
+ # # But, for a better user experience, this is rarely done and more
192
+ # # common way is to ignore the Accept-Language header in this case.
193
+ # return I18n.default_locale if locale.nil?
194
+ #
195
+ # locale
196
+ # end
197
+ # end
198
+ #
199
+ # @see Parser
200
+ # @see https://tools.ietf.org/html/rfc2616#section-14.4 RFC 2616 Section 14.4
201
+ # @see https://tools.ietf.org/html/bcp47 BCP 47
202
+ # @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language MDN Accept-Language
203
+ #
204
+ # @author Cyril Kato
205
+ # @since 1.0.0
16
206
  module AcceptLanguage
17
- # Parses an Accept-Language header field value.
207
+ # Parses an +Accept-Language+ header field value and returns a parser
208
+ # instance that can be used to match against available languages.
209
+ #
210
+ # The parser handles all aspects of the Accept-Language specification:
211
+ # - Quality values (+q=0+ to +q=1+, default +1+ when omitted)
212
+ # - Language tag validation per BCP 47
213
+ # - Wildcards (+*+)
214
+ # - Case normalization (matching is case-insensitive)
215
+ #
216
+ # Invalid language tags or malformed quality values in the input are
217
+ # silently ignored, allowing the parser to handle real-world headers
218
+ # that may not strictly conform to specifications.
219
+ #
220
+ # @param field [String, nil] the Accept-Language header field value.
221
+ # Typically obtained from +request.headers["HTTP_ACCEPT_LANGUAGE"]+ in
222
+ # Rails or +env["HTTP_ACCEPT_LANGUAGE"]+ in Rack applications.
223
+ # When +nil+ is passed (header absent), it is treated as an empty string,
224
+ # resulting in a parser that matches no languages.
225
+ #
226
+ # @return [Parser] a parser instance configured with the language preferences
227
+ # from the header. Call {Parser#match} on this instance to find the best
228
+ # matching language from your available options.
229
+ #
230
+ # @raise [TypeError] if +field+ is neither a String nor +nil+
231
+ #
232
+ # @example Basic parsing and matching
233
+ # parser = AcceptLanguage.parse("en-GB, en;q=0.9, fr;q=0.8")
234
+ # parser.match(:en, :"en-GB", :fr)
235
+ # # => :"en-GB"
236
+ #
237
+ # @example Handling missing or empty headers
238
+ # AcceptLanguage.parse("").match(:en, :fr)
239
+ # # => nil
240
+ #
241
+ # AcceptLanguage.parse(nil).match(:en, :fr)
242
+ # # => nil
243
+ #
244
+ # @example Reusing a parser instance
245
+ # # Parse once, match multiple times
246
+ # user_prefs = AcceptLanguage.parse(header_value)
247
+ #
248
+ # # Match for UI language
249
+ # ui_locale = user_prefs.match(*available_ui_locales)
250
+ #
251
+ # # Match for content language
252
+ # content_locale = user_prefs.match(*available_content_locales)
253
+ #
254
+ # @example Handling edge cases
255
+ # # Invalid q-values are ignored
256
+ # AcceptLanguage.parse("en;q=2.0, fr;q=0.8").match(:en, :fr)
257
+ # # => :fr (en is ignored due to invalid q-value > 1)
18
258
  #
19
- # @param field [String] The Accept-Language header field value
20
- # @return [Parser] A parser object that responds to {Parser#match}
259
+ # # Invalid language tags are ignored
260
+ # AcceptLanguage.parse("123invalid, fr;q=0.8").match(:fr)
261
+ # # => :fr
21
262
  #
22
- # @example
23
- # parser = AcceptLanguage.parse("en-GB, en;q=0.9")
24
- # parser.match(:en, :"en-GB") # => :"en-GB"
263
+ # @see Parser#match
264
+ # @see https://tools.ietf.org/html/rfc2616#section-14.4 RFC 2616 Section 14.4
25
265
  def self.parse(field)
26
266
  Parser.new(field)
27
267
  end
metadata CHANGED
@@ -1,31 +1,17 @@
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.2.0
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
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bigdecimal
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- description: Parses the Accept-Language header from an HTTP request and produces a
28
- hash of languages and qualities.
11
+ date: 2026-01-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A lightweight, thread-safe Ruby library for parsing the Accept-Language
14
+ HTTP header as defined in RFC 2616, with full support for BCP 47 language tags.
29
15
  email: contact@cyril.email
30
16
  executables: []
31
17
  extensions: []
@@ -41,6 +27,10 @@ licenses:
41
27
  - MIT
42
28
  metadata:
43
29
  rubygems_mfa_required: 'true'
30
+ source_code_uri: https://github.com/cyril/accept_language.rb
31
+ documentation_uri: https://rubydoc.info/github/cyril/accept_language.rb/main
32
+ bug_tracker_uri: https://github.com/cyril/accept_language.rb/issues
33
+ wiki_uri: https://github.com/cyril/accept_language.rb/wiki
44
34
  post_install_message:
45
35
  rdoc_options: []
46
36
  require_paths:
@@ -59,5 +49,5 @@ requirements: []
59
49
  rubygems_version: 3.4.19
60
50
  signing_key:
61
51
  specification_version: 4
62
- summary: "Parser for Accept-Language request HTTP header \U0001F310"
52
+ summary: Parser for Accept-Language request HTTP header
63
53
  test_files: []