Skip to content

Commit

Permalink
docs(README, code): add explanation of output after each example
Browse files Browse the repository at this point in the history
  • Loading branch information
cyril committed May 24, 2023
1 parent d48159b commit 9a651d7
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 98 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
- 2.7
- 3.0
- 3.1
- 3.2
- head

steps:
Expand Down
4 changes: 1 addition & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ AllCops:

- doc/

- pkg/


NewCops: enable
TargetRubyVersion: 2.7
TargetRubyVersion: 3.1

inherit_from:
- https://raw.githubusercontent.com/sashite/sashite-rubocop.rb/v1.0.3/config/rubocop.yml
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ AllCops:
<% end %>

NewCops: enable
TargetRubyVersion: 2.7
TargetRubyVersion: 3.1

inherit_from:
- https://raw.githubusercontent.com/sashite/sashite-rubocop.rb/v1.0.3/config/rubocop.yml
Expand Down
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.7.6
3.1.4
12 changes: 12 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@
source "https://rubygems.org"

gemspec

gem "bundler"
gem "rake"
gem "r_spec"
gem "rubocop-gitlab-security"
gem "rubocop-md"
gem "rubocop-performance"
gem "rubocop-rake"
gem "rubocop-rspec"
gem "rubocop-thread_safety"
gem "simplecov"
gem "yard"
58 changes: 31 additions & 27 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
accept_language (2.0.3)
accept_language (2.0.4)

GEM
remote: https://rubygems.org/
Expand All @@ -12,62 +12,66 @@ GEM
aw (~> 0.2.0)
docile (1.4.0)
expresenter (1.4.0)
json (2.6.2)
json (2.6.3)
matchi (3.3.1)
parallel (1.22.1)
parser (3.1.2.1)
parallel (1.23.0)
parser (3.2.2.1)
ast (~> 2.4.1)
r_spec (1.0.5)
r_spec-clone
r_spec-clone (1.6.0)
aw (~> 0.2.0)
r_spec-clone (1.7.0)
expresenter (~> 1.4.0)
matchi (~> 3.3.0)
matchi (~> 3.3.1)
test_tube (~> 2.1.3)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.5.0)
regexp_parser (2.8.0)
rexml (3.2.5)
rubocop (1.35.0)
rubocop (1.51.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.20.1, < 2.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.21.0)
parser (>= 3.1.1.0)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.1)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-md (1.0.1)
rubocop-md (1.2.0)
rubocop (>= 1.0)
rubocop-performance (1.14.3)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rake (0.6.0)
rubocop (~> 1.0)
rubocop-rspec (2.12.1)
rubocop (~> 1.31)
rubocop-thread_safety (0.4.4)
rubocop (>= 0.53.0)
ruby-progressbar (1.11.0)
simplecov (0.21.2)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
rubocop-thread_safety (0.5.1)
rubocop (>= 0.90.0)
ruby-progressbar (1.13.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
test_tube (2.1.3)
defi (~> 2.0.6)
unicode-display_width (2.2.0)
webrick (1.7.0)
yard (0.9.28)
webrick (~> 1.7.0)
unicode-display_width (2.4.2)
yard (0.9.34)

PLATFORMS
ruby
x86_64-linux

DEPENDENCIES
Expand All @@ -85,4 +89,4 @@ DEPENDENCIES
yard

BUNDLED WITH
2.3.19
2.3.26
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The MIT License

Copyright (c) 2019-2022 Cyril Kato
Copyright (c) 2019-2023 Cyril Kato

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
69 changes: 46 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Accept Language 🌐

A tiny library for parsing the `Accept-Language` header from browsers (as defined in [RFC 2616](https://tools.ietf.org/html/rfc2616#section-14.4)).
Web applications often need to cater to users from around the world. One of the ways they can provide a better user experience is by presenting the content in the user's preferred language. This is where the `Accept-Language` HTTP header comes into play. Sent by the client (usually a web browser), this header tells the server the list of languages the user understands, and the user's preference order.

Parsing the `Accept-Language` header can be complex due to its flexible format defined in [RFC 2616](https://tools.ietf.org/html/rfc2616#section-14.4). For instance, it can specify languages, countries, and scripts with varying degrees of preference (quality values).

`Accept Language` is a lightweight, thread-safe Ruby library designed to parse the `Accept-Language` header, making it easier for your application to determine the best language to respond with. It calculates the intersection of the languages the user prefers and the languages your application supports, handling all the complexity of quality values and wildcards.

Whether you're building a multilingual web application or just trying to make your service more accessible to users worldwide, `Accept Language` offers a reliable, simple solution.

## Status

Expand All @@ -12,11 +18,15 @@ A tiny library for parsing the `Accept-Language` header from browsers (as define

## Why this tool?

- Thread-safe implementation.
- Small algorithm that can handle tricky cases.
- Match strings and symbols ignoring the case.
- Works also well without Rails, Rack, i18n.
- Comes with [BCP 47](https://www.rfc-editor.org/bcp/bcp47.txt) support.
## Why Choose Accept Language?

There are a myriad of tools out there, so why should you consider Accept Language for your next project? Here's why:

- **Thread-Safe**: Multithreading can present unique challenges when dealing with shared resources. Our implementation is designed to be thread-safe, preventing potential concurrency issues.
- **Compact and Robust**: Despite being small in size, Accept Language can handle even the trickiest cases with grace, ensuring you have a reliable tool at your disposal.
- **Case-Insensitive Matching**: In line with the principle of robustness, our tool matches both strings and symbols regardless of case, providing greater flexibility.
- **Independent of Framework**: While many tools require Rails, Rack, or i18n to function, Accept Language stands on its own. It works perfectly well without these dependencies, increasing its adaptability.
- **BCP 47 Support**: BCP 47 defines a standard for language tags. This is crucial for specifying languages unambiguously. Accept Language complies with this standard, ensuring accurate language identification.

## Installation

Expand All @@ -40,28 +50,41 @@ gem install accept_language

## Usage

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.

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.
`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.

Some examples:
Below are some examples of how you might use the library:

```ruby
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da) # => :da
AcceptLanguage.parse("da, en;q=0.8, ug;q=0.9").match("en-GB", "ug-CN") # => "ug-CN"
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:ja) # => nil
AcceptLanguage.parse("fr-CH").match(:fr) # => nil
AcceptLanguage.parse("de, zh;q=0.4, fr;q=0").match(:fr) # => nil
AcceptLanguage.parse("de, zh;q=0.4, *;q=0.5, fr;q=0").match(:ar) # => :ar
AcceptLanguage.parse("uz-latn-uz").match("uz-Latn-UZ") # => "uz-Latn-UZ"
AcceptLanguage.parse("foo;q=0.1").match(:FoO) # => :FoO
AcceptLanguage.parse("foo").match("bar") # => nil
AcceptLanguage.parse("*").match("BaZ") # => "BaZ"
AcceptLanguage.parse("*;q=0").match("foobar") # => nil
AcceptLanguage.parse("en, en;q=0").match("en") # => nil
AcceptLanguage.parse("*, en;q=0").match("en") # => nil
# The user prefers Danish, then British English, and finally any kind of English.
# Since your application supports English and Danish, it selects Danish as it's the user's first choice.
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da) # => :da

# The user prefers Danish, then English, and finally Uyghur. Your application supports British English and Chinese Uyghur.
# Here, the library will return Chinese Uyghur because it's the highest ranked language in the user's list that your application supports.
AcceptLanguage.parse("da, en;q=0.8, ug;q=0.9").match("en-GB", "ug-CN") # => "ug-CN"

# The user prefers Danish, then British English, and finally any kind of English. Your application only supports Japanese.
# Since none of the user's preferred languages are supported, it returns nil.
AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:ja) # => nil

# The user only accepts Swiss French, but your application only supports French. Since Swiss French and French are not the same, it returns nil.
AcceptLanguage.parse("fr-CH").match(:fr) # => nil

# The user prefers German, then any language except French. Your application supports French.
# Even though the user specified a wildcard, they explicitly excluded French. Therefore, it returns nil.
AcceptLanguage.parse("de, zh;q=0.4, *;q=0.5, fr;q=0").match(:fr) # => nil

# The user prefers Uyghur (in Latin script, as used in Uzbekistan). Your application supports this exact variant of Uyghur.
# Since the user's first choice matches a language your application supports, it returns that language.
AcceptLanguage.parse("uz-latn-uz").match("uz-Latn-UZ") # => "uz-Latn-UZ"

# The user doesn't mind what language they get, but they'd prefer not to have English. Your application supports English.
# Even though the user specified a wildcard, they explicitly excluded English. Therefore, it returns nil.
AcceptLanguage.parse("*, en;q=0").match("en") # => nil
```

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.

### Rails integration example

```ruby
Expand Down
2 changes: 1 addition & 1 deletion VERSION.semver
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.3
2.0.4
14 changes: 1 addition & 13 deletions accept_language.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,7 @@ Gem::Specification.new do |spec|
spec.license = "MIT"
spec.files = Dir["LICENSE.md", "README.md", "lib/**/*"]

spec.required_ruby_version = ">= 2.7.6"

spec.add_development_dependency "bundler"
spec.add_development_dependency "rake"
spec.add_development_dependency "r_spec"
spec.add_development_dependency "rubocop-gitlab-security"
spec.add_development_dependency "rubocop-md"
spec.add_development_dependency "rubocop-performance"
spec.add_development_dependency "rubocop-rake"
spec.add_development_dependency "rubocop-rspec"
spec.add_development_dependency "rubocop-thread_safety"
spec.add_development_dependency "simplecov"
spec.add_development_dependency "yard"
spec.required_ruby_version = ">= 3.1.4"

spec.metadata["rubygems_mfa_required"] = "true"
end
24 changes: 19 additions & 5 deletions lib/accept_language.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
# frozen_string_literal: true

# Tiny library for parsing the Accept-Language header.
# This module provides a tiny library for parsing the Accept-Language header as specified in RFC 2616.
# It transforms the Accept-Language header field into a language range, providing a flexible way to determine
# user's language preferences and match them with the available languages in your application.
#
# @example
# 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}>
# AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7")
# # => #<AcceptLanguage::Parser:0x00007 @languages_range={"da"=>1.0, "en-GB"=>0.8, "en"=>0.7}>
#
# @see https://tools.ietf.org/html/rfc2616#section-14.4
module AcceptLanguage
# @note Parse an Accept-Language header field into a language range.
# Parses an Accept-Language header field value into a Parser object, which can then be used to match
# user's preferred languages against the languages your application supports.
# This method accepts a string argument in the format as described in RFC 2616 Section 14.4, and returns
# a Parser object which responds to the #match method.
#
# @param field [String] the Accept-Language header field value.
#
# @example
# 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}>
# @return [#match] a parser that responds to #match.
# AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7")
# # => #<AcceptLanguage::Parser:0x00007 @languages_range={"da"=>1.0, "en-GB"=>0.8, "en"=>0.7}>
#
# @return [Parser] a Parser object that responds to #match method.
def self.parse(field)
Parser.new(field)
end
end

# Load the Parser class
require_relative File.join("accept_language", "parser")
26 changes: 17 additions & 9 deletions lib/accept_language/matcher.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# frozen_string_literal: true

module AcceptLanguage
# @note Compare an Accept-Language header value with your application's
# supported languages to find the common languages that could be presented
# to a user.
# A utility class that provides functionality to match the Accept-Language header value
# against the languages supported by your application. This helps in identifying the most
# suitable languages to present to the user based on their preferences.
#
# @example
# Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:ug, :kk, :ru, :en) # => :en
# Matcher.new("da" => 1.0, "en-GB" => 0.8, "en" => 0.7).call(:fr, :en, :"en-GB") # => :"en-GB"
Expand All @@ -12,8 +13,11 @@ class Matcher

attr_reader :excluded_langtags, :preferred_langtags

# @param [Hash<String, BigDecimal>] languages_range A list of accepted
# languages with their respective qualities.
# Initialize a new Matcher object with the languages_range parameter representing the user's
# preferred languages and their respective quality values.
#
# @param [Hash<String, BigDecimal>] languages_range A hash where keys represent languages and
# values are the quality of preference for each language. A value of zero means the language is not acceptable.
def initialize(**languages_range)
@excluded_langtags = ::Set[]
langtags = []
Expand All @@ -30,11 +34,15 @@ def initialize(**languages_range)
@preferred_langtags = langtags.compact.reverse
end

# @param [Array<String, Symbol>] available_langtags The list of available
# languages.
# @example Uyghur, Kazakh, Russian and English languages are available.
# Matches the user's preferred languages against the available languages of your application.
# It prioritizes higher quality values and returns the most suitable match.
#
# @param [Array<String, Symbol>] available_langtags An array representing the languages available in your application.
#
# @example When Uyghur, Kazakh, Russian and English languages are available.
# call(:ug, :kk, :ru, :en)
# @return [String, Symbol, nil] The language that best matches.
#
# @return [String, Symbol, nil] The language that best matches the user's preferences, or nil if there is no match.
def call(*available_langtags)
available_langtags = drop_unacceptable(*available_langtags)

Expand Down
Loading

0 comments on commit 9a651d7

Please sign in to comment.