accept_language 1.0.1 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b2ef71586c1b52c839323443779e12b8feff9b54aa818b11d427762b1fac5ae
4
- data.tar.gz: 99f4391638e32daa51aac44ca008cef6bfaf37a4a40a3ffdab10959dcb023753
3
+ metadata.gz: b1b6659d931a56171b880e543a4364fdd0212cf1664d9fa32d04f29c9142b1a6
4
+ data.tar.gz: 259ddc12dc7ef62325a5fe634dd0bcb284f1dce0d9629d929858751dcc8cd936
5
5
  SHA512:
6
- metadata.gz: 6b61d1f98342c9e8e90bd72c9b3c906a9b52c01f8adb7a1edd7165a6303c61dbd8e202cfdc45eb9d8f1d57ab855052bc2d8c6bfac4e73c2b4a3df95be70bed24
7
- data.tar.gz: 2cd335536235546b9c95b2ebcca1f6cfadfa9a0c9d03b5d33f3cc1a6b8965bec958ada7a79c78248ca7f8d7aa083ee42d3e5181b1496f717d99f9768c1c94213
6
+ metadata.gz: c1566e1af8e2cd77e96eebebf25e7f694237f1562396d42ee001349d6fb75503fe022a8325e208db9cd24e354d90253496e11af66f1bd5b7704e8b54ac347263
7
+ data.tar.gz: 0d7d2c59183e9eae327e61f4ccae25c050f5a82eb511041b2c86c4478548c7144ea10fa78f9c9a4d4b501fdbf8375b72e001e77f73d0820272c139fa9dad5972
data/README.md CHANGED
@@ -28,25 +28,24 @@ Or install it yourself as:
28
28
 
29
29
  It's intended to be used in a Web server that supports some level of internationalization (i18n), but can be used anytime an `Accept-Language` header string is available.
30
30
 
31
- Examples:
31
+ In order to help facilitate better i18n, the lib try to find the intersection of the languages the user prefers and the languages your application supports.
32
32
 
33
- ```ruby
34
- AcceptLanguage.parse("da, en-gb;q=0.8, en;q=0.7") # => {:da=>1.0, :"en-gb"=>0.8, :en=>0.7}
35
- AcceptLanguage.parse("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") # => {:"fr-ch"=>1.0, :fr=>0.9, :en=>0.8, :de=>0.7, :*=>0.5}
36
- ```
37
-
38
- In order to help facilitate better i18n, a method is provided to return the intersection of the languages the user prefers and the languages your application supports.
39
-
40
- Examples:
33
+ Some examples:
41
34
 
42
35
  ```ruby
43
- AcceptLanguage.intersection("da, en-gb;q=0.8, en;q=0.7", :ja, :ro, :da) # => :da
44
- AcceptLanguage.intersection("da, en-gb;q=0.8, en;q=0.7", :ja, :ro) # => nil
45
- AcceptLanguage.intersection("fr-CH", :fr, two_letter_truncate: false) # => nil
46
- AcceptLanguage.intersection("fr-CH", :fr, two_letter_truncate: true) # => :fr
47
- AcceptLanguage.intersection("de, zh;q=0.4, fr;q=0", :fr) # => nil
48
- AcceptLanguage.intersection("de, zh;q=0.4, *;q=0.5, fr;q=0", :fr) # => nil
49
- AcceptLanguage.intersection("de, zh;q=0.4, *;q=0.5, fr;q=0", :ar) # => :ar
36
+ AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da) # => :da
37
+ AcceptLanguage.parse("da, en;q=0.8, ug;q=0.9").match("en-GB", "ug-CN") # => "ug-CN"
38
+ AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:ja) # => nil
39
+ AcceptLanguage.parse("fr-CH").match(:fr) # => nil
40
+ AcceptLanguage.parse("de, zh;q=0.4, fr;q=0").match(:fr) # => nil
41
+ AcceptLanguage.parse("de, zh;q=0.4, *;q=0.5, fr;q=0").match(:ar) # => :ar
42
+ AcceptLanguage.parse("uz-latn-uz").match("uz-Latn-UZ") # => "uz-Latn-UZ"
43
+ AcceptLanguage.parse("foo;q=0.1").match(:FoO) # => :FoO
44
+ AcceptLanguage.parse("foo").match("bar") # => nil
45
+ AcceptLanguage.parse("*").match("BaZ") # => "BaZ"
46
+ AcceptLanguage.parse("*;q=0").match("foobar") # => nil
47
+ AcceptLanguage.parse("en, en;q=0").match("en") # => nil
48
+ AcceptLanguage.parse("*, en;q=0").match("en") # => nil
50
49
  ```
51
50
 
52
51
  ### Rails integration example
@@ -64,7 +63,7 @@ class ApplicationController < ActionController::Base
64
63
  return I18n.default_locale unless request.headers.key?("HTTP_ACCEPT_LANGUAGE")
65
64
 
66
65
  string = request.headers.fetch("HTTP_ACCEPT_LANGUAGE")
67
- locale = AcceptLanguage.intersection(string, I18n.default_locale, *I18n.available_locales)
66
+ locale = AcceptLanguage.parse(string).match(*I18n.available_locales)
68
67
 
69
68
  # If the server cannot serve any matching language,
70
69
  # it can theoretically send back a 406 (Not Acceptable) error code.
@@ -1,18 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Tiny library for parsing the Accept-Language header.
4
+ # @example
5
+ # AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7") # => #<AcceptLanguage::Parser:0x00007 @languages_range={"da"=>0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}>
6
+ # @see https://tools.ietf.org/html/rfc2616#section-14.4
4
7
  module AcceptLanguage
8
+ # @note Parse an Accept-Language header field into a language range.
5
9
  # @example
6
- # AcceptLanguage.intersection('ja, en-gb;q=0.8, en;q=0.7', :ar, :ja) # => :ja
7
- def self.intersection(raw_input, *supported_langs, two_letter_truncate: true)
8
- Intersection.new(raw_input, *supported_langs, two_letter_truncate: two_letter_truncate).call
9
- end
10
-
11
- # @example
12
- # AcceptLanguage.parse('ja, en-gb;q=0.8, en;q=0.7') # => { ja: 1.0, "en-gb": 0.8, en: 0.7 }
13
- def self.parse(raw_input, two_letter_truncate: false)
14
- Parser.call(raw_input, two_letter_truncate: two_letter_truncate)
10
+ # parse("da, en-GB;q=0.8, en;q=0.7") # => #<AcceptLanguage::Parser:0x00007 @languages_range={"da"=>0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}>
11
+ # @return [#match] a parser that responds to #match.
12
+ def self.parse(field)
13
+ Parser.new(field)
15
14
  end
16
15
  end
17
16
 
18
- require_relative "accept_language/intersection"
17
+ require_relative "accept_language/parser"
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AcceptLanguage
4
+ # @note Compare an Accept-Language header value with your application's
5
+ # supported languages to find the common languages that could be presented
6
+ # to a user.
7
+ # @example
8
+ # Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:ug, :kk, :ru, :en) # => :en
9
+ # Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:fr, :en, :"en-GB") # => :"en-GB"
10
+ class Matcher
11
+ attr_reader :excluded_langtags, :preferred_langtags
12
+
13
+ # @param [Hash<String, BigDecimal>] languages_range A list of accepted
14
+ # languages with their respective qualities.
15
+ def initialize(**languages_range)
16
+ @excluded_langtags = Set[]
17
+ langtags = []
18
+
19
+ languages_range.select do |langtag, quality|
20
+ if quality.zero?
21
+ @excluded_langtags << langtag unless langtag.eql?("*")
22
+ else
23
+ level = (quality * 1_000).to_i
24
+ langtags[level] = langtag
25
+ end
26
+ end
27
+
28
+ @preferred_langtags = langtags.compact.reverse
29
+ end
30
+
31
+ # @param [Array<String, Symbol>] available_langtags The list of available
32
+ # languages.
33
+ # @example Uyghur, Kazakh, Russian and English languages are available.
34
+ # call(:ug, :kk, :ru, :en)
35
+ # @return [String, Symbol, nil] The language that best matches.
36
+ def call(*available_langtags)
37
+ available_langtags = drop_unacceptable(*available_langtags)
38
+
39
+ preferred_langtags.each do |preferred_tag|
40
+ if preferred_tag.eql?("*")
41
+ langtag = any_other_langtag(*available_langtags)
42
+ return langtag unless langtag.nil?
43
+ else
44
+ available_langtags.each do |available_langtag|
45
+ return available_langtag if available_langtag.match?(/\A#{preferred_tag}/i)
46
+ end
47
+ end
48
+ end
49
+
50
+ nil
51
+ end
52
+
53
+ private
54
+
55
+ def any_other_langtag(*available_langtags)
56
+ available_langtags.find do |available_langtag|
57
+ langtags = preferred_langtags - ["*"]
58
+
59
+ langtags.none? do |langtag|
60
+ available_langtag.match?(/\A#{langtag}/i)
61
+ end
62
+ end
63
+ end
64
+
65
+ def drop_unacceptable(*available_langtags)
66
+ available_langtags.inject(Set[]) do |langtags, available_langtag|
67
+ next langtags if unacceptable?(available_langtag)
68
+
69
+ langtags + Set[available_langtag]
70
+ end
71
+ end
72
+
73
+ def unacceptable?(langtag)
74
+ excluded_langtags.any? do |excluded_langtag|
75
+ langtag.match?(/\A#{excluded_langtag}/i)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ require "set"
@@ -1,26 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AcceptLanguage
4
+ # @note Parser for Accept-Language header fields.
4
5
  # @example
5
- # AcceptLanguage::Parser.call('da, en-gb;q=0.8, en;q=0.7') # => { da: 1.0, "en-gb": 0.8, en: 0.7 }
6
- # @note Parse an Accept-Language header value into a hash of tag and quality.
6
+ # Parser.new("da, en-GB;q=0.8, en;q=0.7") # => #<AcceptLanguage::Parser:0x00007 @languages_range={"da"=>0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}>
7
7
  # @see https://tools.ietf.org/html/rfc2616#section-14.4
8
- module Parser
9
- def self.call(raw_input, two_letter_truncate: false)
10
- raw_input.to_s.delete(" ").split(",").inject({}) do |hash, lang|
11
- tag, quality = lang.split(/;q=/i)
12
- next hash if tag.nil?
8
+ class Parser
9
+ attr_reader :languages_range
13
10
 
14
- tag = tag.downcase.to_sym
11
+ # @param [String] field The Accept-Language header field to parse.
12
+ # @see https://tools.ietf.org/html/rfc2616#section-14.4
13
+ def initialize(field)
14
+ @languages_range = import(field)
15
+ end
16
+
17
+ # @param [Array<String, Symbol>] available_langtags The list of available
18
+ # languages.
19
+ # @example Uyghur, Kazakh, Russian and English languages are available.
20
+ # match(:ug, :kk, :ru, :en)
21
+ # @return [String, Symbol, nil] The language that best matches.
22
+ def match(*available_langtags)
23
+ Matcher.new(**languages_range).call(*available_langtags)
24
+ end
15
25
 
16
- if two_letter_truncate && tag.length > 2
17
- tag = tag[0, 2].to_sym
18
- next hash if hash.key?(tag)
19
- end
26
+ private
20
27
 
21
- quality = quality.nil? ? 1.0 : quality.to_f
28
+ # @example
29
+ # import('da, en-GB;q=0.8, en;q=0.7') # => {"da"=>0.1e1, "en-GB"=>0.8e0, "en"=>0.7e0}
30
+ # @return [Hash<String, BigDecimal>] A list of accepted languages with their
31
+ # respective qualities.
32
+ def import(field)
33
+ field.delete(" ").split(",").inject({}) do |hash, lang|
34
+ tag, quality = lang.split(/;q=/i)
35
+ next hash if tag.nil?
36
+
37
+ quality = quality.nil? ? BigDecimal("1") : BigDecimal(quality)
22
38
  hash.merge(tag => quality)
23
39
  end
24
40
  end
25
41
  end
26
42
  end
43
+
44
+ require "bigdecimal"
45
+ require_relative "matcher"
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: 1.0.1
4
+ version: 2.0.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: 2021-05-01 00:00:00.000000000 Z
11
+ date: 2021-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -160,7 +160,7 @@ files:
160
160
  - LICENSE.md
161
161
  - README.md
162
162
  - lib/accept_language.rb
163
- - lib/accept_language/intersection.rb
163
+ - lib/accept_language/matcher.rb
164
164
  - lib/accept_language/parser.rb
165
165
  homepage: https://github.com/cyril/accept_language.rb
166
166
  licenses:
@@ -174,7 +174,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
174
174
  requirements:
175
175
  - - ">="
176
176
  - !ruby/object:Gem::Version
177
- version: 2.7.0
177
+ version: 3.0.0
178
178
  required_rubygems_version: !ruby/object:Gem::Requirement
179
179
  requirements:
180
180
  - - ">="
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AcceptLanguage
4
- # @example
5
- # AcceptLanguage::Intersection.new('ja, en-gb;q=0.8, en;q=0.7', :ar, :ja).call # => :ja
6
- # @note Compare an Accept-Language header value with your application's
7
- # supported languages to find the common languages that could be presented
8
- # to a user.
9
- # @see https://tools.ietf.org/html/rfc2616#section-14.4
10
- class Intersection
11
- attr_reader :preferences, :supported_langs
12
-
13
- def initialize(raw_input, *supported_langs, two_letter_truncate: true)
14
- @preferences = Parser.call(raw_input, two_letter_truncate: two_letter_truncate)
15
-
16
- @supported_langs = supported_langs.map do |lang|
17
- lang = lang.downcase
18
- lang = lang[0, 2] if two_letter_truncate
19
- lang.to_sym
20
- end.uniq
21
- end
22
-
23
- def call
24
- qualities_without_zero_in_desc_order.each do |quality|
25
- tag = preferences.key(quality)
26
-
27
- if wildcard?(tag)
28
- lang = any_tag_not_matched_by_any_other_range
29
- return lang unless lang.nil?
30
- end
31
-
32
- return tag if supported_langs.include?(tag)
33
- end
34
-
35
- nil
36
- end
37
-
38
- protected
39
-
40
- def any_tag_not_matched_by_any_other_range
41
- every_tag_not_matched_by_any_other_range.first
42
- end
43
-
44
- def every_tag_not_matched_by_any_other_range
45
- supported_langs - preferences.keys
46
- end
47
-
48
- def qualities_without_zero_in_desc_order
49
- preferences.values.reject(&:zero?).uniq.sort.reverse
50
- end
51
-
52
- def wildcard?(value)
53
- value.equal?(:*)
54
- end
55
- end
56
- end
57
-
58
- require_relative "parser"