接受语言 2.2 – 符合 RFC 7231/4647 标准的 Ruby 语言接受解析
Accept_language 2.2 – RFC 7231/4647 compliant Accept-Language parsing for Ruby

原始链接: https://github.com/cyril/accept_language.rb

## Accept-Language Ruby 库总结 这个 Ruby 库提供了一个健壮且线程安全的 HTTP `Accept-Language` 头部解析器,完全符合 RFC 7231 和 RFC 4647 标准。它准确地解析通过质量值(q 值,0-1,默认值为 1)表达的语言偏好,并在 q 值相等时根据声明顺序对语言进行优先级排序。 该解析器使用基本的过滤匹配方案,支持语言标签的前缀匹配(例如,“de”匹配“de-DE”)。它还支持通配符 (*) 以匹配任何未明确排除的语言,并处理使用 q=0 的显式排除。匹配不区分大小写,并保留匹配标签的原始大小写。 该库完全支持 BCP 47 语言标签,包括脚本、地区和变体子标签。它专为诸如 Web 服务器(通过 Rack 中间件和 Rails 控制器示例演示)之类的应用程序设计,以确定基于用户偏好的最佳语言环境。它遵循语义化版本控制,并以 MIT 许可证开源。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Accept-Language 2.2 – 符合 RFC 7231/4647 标准的 Ruby Accept-Language 解析 (github.com/cyril) 5 分,作者 cyrilllllll 1 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

A lightweight, thread-safe Ruby library for parsing the Accept-Language HTTP header field.

This implementation conforms to:

Note

RFC 7231 obsoletes RFC 2616 (the original HTTP/1.1 specification). The Accept-Language header behavior defined in RFC 2616 Section 14.4 remains unchanged in RFC 7231, ensuring full backward compatibility.

Version Yard documentation Ruby RuboCop License

AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7").match(:en, :da)
# => :da

Quality values (q-values) indicate relative preference, ranging from 0 (not acceptable) to 1 (most preferred). When omitted, the default is 1.

Per RFC 7231 Section 5.3.1, valid q-values have at most three decimal places: 0, 0.7, 0.85, 1.000. Invalid q-values cause the associated language range to be ignored.

parser = AcceptLanguage.parse("da, en-GB;q=0.8, en;q=0.7")

parser.match(:en, :da)      # => :da       (q=1 beats q=0.8)
parser.match(:en, :"en-GB") # => :"en-GB"  (q=0.8 beats q=0.7)
parser.match(:ja)           # => nil       (no match)

When multiple languages share the same q-value, declaration order in the header determines priority—the first declared language wins:

AcceptLanguage.parse("en;q=0.8, fr;q=0.8").match(:en, :fr)
# => :en  (declared first)

AcceptLanguage.parse("fr;q=0.8, en;q=0.8").match(:en, :fr)
# => :fr  (declared first)

This library implements the Basic Filtering matching scheme defined in RFC 4647 Section 3.3.1. A language range matches a language tag if, in a case-insensitive comparison, it exactly equals the tag, or if it exactly equals a prefix of the tag such that the first character following the prefix is -.

AcceptLanguage.parse("de-de").match(:"de-DE-1996")
# => :"de-DE-1996"  (prefix match)

AcceptLanguage.parse("de-de").match(:"de-Deva")
# => nil  ("de-de" is not a prefix of "de-Deva")

AcceptLanguage.parse("de-de").match(:"de-Latn-DE")
# => nil  ("de-de" is not a prefix of "de-Latn-DE")

Prefix matching respects hyphen boundaries:

AcceptLanguage.parse("zh").match(:"zh-TW")
# => :"zh-TW"  ("zh" matches "zh-TW")

AcceptLanguage.parse("zh").match(:zhx)
# => nil  ("zh" does not match "zhx" — different language code)

AcceptLanguage.parse("zh-TW").match(:zh)
# => nil  (more specific range does not match less specific tag)

The wildcard * matches any language not matched by another range in the header. This behavior is specific to HTTP, as noted in RFC 4647 Section 3.3.1.

AcceptLanguage.parse("de, *;q=0.5").match(:ja)
# => :ja  (matched by wildcard)

AcceptLanguage.parse("de, *;q=0.5").match(:de, :ja)
# => :de  (explicit match takes precedence)

A q-value of 0 explicitly marks a language as not acceptable:

AcceptLanguage.parse("*, en;q=0").match(:en)
# => nil  (English explicitly excluded)

AcceptLanguage.parse("*, en;q=0").match(:ja)
# => :ja  (Japanese matched by wildcard)

Exclusions apply via prefix matching:

AcceptLanguage.parse("*, en;q=0").match(:"en-GB")
# => nil  (en-GB excluded via "en" prefix)

Matching is case-insensitive per RFC 4647 Section 2, but the original case of available language tags is preserved in the return value:

AcceptLanguage.parse("EN-GB").match(:"en-gb")
# => :"en-gb"

AcceptLanguage.parse("en-gb").match(:"EN-GB")
# => :"EN-GB"

Full support for BCP 47 language tags including script subtags, region subtags, and variant subtags:

# Script subtags
AcceptLanguage.parse("zh-Hant").match(:"zh-Hant-TW", :"zh-Hans-CN")
# => :"zh-Hant-TW"

# Variant subtags
AcceptLanguage.parse("de-1996, de;q=0.9").match(:"de-CH-1996", :"de-CH")
# => :"de-CH-1996"
# config.ru
class LocaleMiddleware
  def initialize(app, available_locales:, default_locale:)
    @app = app
    @available_locales = available_locales
    @default_locale = default_locale
  end

  def call(env)
    locale = detect_locale(env) || @default_locale
    env["rack.locale"] = locale
    @app.call(env)
  end

  private

  def detect_locale(env)
    header = env["HTTP_ACCEPT_LANGUAGE"]
    return unless header

    AcceptLanguage.parse(header).match(*@available_locales)
  end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :best_locale_from_request!

  def best_locale_from_request!
    I18n.locale = best_locale_from_request
  end

  def best_locale_from_request
    # HTTP_ACCEPT_LANGUAGE is the standardized key for the Accept-Language header in Rack/Rails
    return I18n.default_locale unless request.headers.key?("HTTP_ACCEPT_LANGUAGE")

    string = request.headers.fetch("HTTP_ACCEPT_LANGUAGE")
    locale = AcceptLanguage.parse(string).match(*I18n.available_locales)

    # If the server cannot serve any matching language,
    # it can theoretically send back a 406 (Not Acceptable) error code.
    # But, for a better user experience, this is rarely done and more
    # common way is to ignore the Accept-Language header in this case.
    return I18n.default_locale if locale.nil?

    locale
  end
end
Specification Description Status
RFC 7231 §5.3.5 Accept-Language header field ✅ Supported
RFC 7231 §5.3.1 Quality values (qvalues) ✅ Supported
RFC 4647 §2.1 Basic Language Range syntax ✅ Supported
RFC 4647 §3.3.1 Basic Filtering scheme ✅ Supported
BCP 47 Language tag structure ✅ Supported
Specification Description Reason
RFC 4647 §2.2 Extended Language Range Not used by HTTP
RFC 4647 §3.3.2 Extended Filtering Not used by HTTP
RFC 4647 §3.4 Lookup scheme Design choice — Basic Filtering is appropriate for HTTP content negotiation

This library follows Semantic Versioning 2.0.

Available as open source under the terms of the MIT License.

联系我们 contact @ memedata.com