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.
- checksums.yaml +4 -4
- data/README.md +119 -31
- data/lib/accept_language/matcher.rb +323 -33
- data/lib/accept_language/parser.rb +397 -27
- data/lib/accept_language.rb +252 -12
- metadata +10 -20
data/lib/accept_language.rb
CHANGED
|
@@ -1,27 +1,267 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
20
|
-
#
|
|
259
|
+
# # Invalid language tags are ignored
|
|
260
|
+
# AcceptLanguage.parse("123invalid, fr;q=0.8").match(:fr)
|
|
261
|
+
# # => :fr
|
|
21
262
|
#
|
|
22
|
-
# @
|
|
23
|
-
#
|
|
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.
|
|
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-
|
|
12
|
-
dependencies:
|
|
13
|
-
-
|
|
14
|
-
|
|
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:
|
|
52
|
+
summary: Parser for Accept-Language request HTTP header
|
|
63
53
|
test_files: []
|