rack-icu4x-locale 0.5.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e9e8d904fcd52b6cff5640053e45c2d75f3cc07a4a0c9db6acfdf402853cdaf4
4
+ data.tar.gz: 8e1ebf2cf313f66211ae47a671e808f09a58d7de2a27b0dd47ddf3d367962893
5
+ SHA512:
6
+ metadata.gz: a22beb45bcf4fa131cd12cd48b760e438e2b10719a05182388311e6cd5241486cf4c862bca0d8d72a03f2d42b604321641964e9adcd82ac931f8117246e868c8
7
+ data.tar.gz: c21cf1f3797a084f08b92fe9bd8d07d30b349bc454e548c6ad2aa06d0ab4e271e8c8cd106a22136e07ddf72d0a38a1999773f7435664a9edfc4f77edd7d2eece
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.5.0] - 2026-01-11
4
+
5
+ ### Added
6
+
7
+ - Pluggable detector system for locale sources (#10)
8
+ - Locale detection from query parameters, cookies, and Accept-Language header
9
+ - Script-safe language negotiation using ICU4X's maximize
10
+ - Sinatra demo application in `examples/`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 OZAWA Sakuro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Rack::ICU4X::Locale
2
+
3
+ Rack middleware for locale detection using ICU4X. Detects user's preferred locales from various sources (query parameters, cookies, Accept-Language header), with script-safe language negotiation.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem 'rack-icu4x-locale'
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Ruby 3.2+
14
+ - Rack 3.0+
15
+
16
+ ## Usage
17
+
18
+ ### Basic (Accept-Language header only)
19
+
20
+ ```ruby
21
+ use Rack::ICU4X::Locale, locales: %w[en ja de fr]
22
+
23
+ run ->(env) {
24
+ locales = env["rack.icu4x.locale"]
25
+ [200, {}, ["Locale: #{locales.first}"]]
26
+ }
27
+ ```
28
+
29
+ ### With Multiple Detectors
30
+
31
+ ```ruby
32
+ use Rack::ICU4X::Locale,
33
+ locales: %w[en ja de fr],
34
+ detectors: [
35
+ {query: "lang"}, # ?lang=ja
36
+ {cookie: "locale"}, # Cookie: locale=ja
37
+ :header # Accept-Language header
38
+ ],
39
+ default: "en" # optional: fallback locale when no match
40
+ ```
41
+
42
+ ### With Custom Detector
43
+
44
+ ```ruby
45
+ use Rack::ICU4X::Locale,
46
+ locales: %w[en ja de fr],
47
+ detectors: [
48
+ ->(env) { env["rack.session"]&.[]("locale") },
49
+ :header
50
+ ]
51
+ ```
52
+
53
+ ## Detector Types
54
+
55
+ | Type | Example | Description |
56
+ |------|---------|-------------|
57
+ | `:header` | `Accept-Language: ja` | Accept-Language header |
58
+ | `{cookie: "name"}` | `Cookie: name=ja` | Cookie value |
59
+ | `{query: "param"}` | `?param=ja` | Query string parameter |
60
+ | `Proc` | `->(env) { ... }` | Custom detection logic |
61
+
62
+ ## Documentation
63
+
64
+ See [doc/specification.md](doc/specification.md) for detailed specification.
65
+
66
+ ## Demo
67
+
68
+ ```bash
69
+ cd examples
70
+ bundle install
71
+ bundle exec rackup
72
+ ```
73
+
74
+ Open http://localhost:9292
75
+
76
+ ## License
77
+
78
+ MIT License. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ICU4X
5
+ class Locale
6
+ module Detector
7
+ # Detects locale from a cookie value.
8
+ #
9
+ # @example With default cookie name
10
+ # detector = Cookie.new
11
+ # env = { "HTTP_COOKIE" => "locale=ja" }
12
+ # detector.call(env) # => "ja"
13
+ #
14
+ # @example With custom cookie name
15
+ # detector = Cookie.new("user_locale")
16
+ # env = { "HTTP_COOKIE" => "user_locale=en" }
17
+ # detector.call(env) # => "en"
18
+ class Cookie
19
+ DEFAULT_NAME = "locale"
20
+ private_constant :DEFAULT_NAME
21
+
22
+ # @param name [String] Cookie name to read locale from
23
+ def initialize(name=DEFAULT_NAME)
24
+ @name = name
25
+ end
26
+
27
+ # @param env [Hash] Rack environment
28
+ # @return [String, nil] Locale from cookie, or nil if not present
29
+ def call(env)
30
+ cookies = ::Rack::Utils.parse_cookies(env)
31
+ locale = cookies[@name]
32
+ return nil if locale.nil? || locale.empty?
33
+
34
+ locale
35
+ end
36
+
37
+ private attr_reader :name
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ICU4X
5
+ class Locale
6
+ module Detector
7
+ # Detects locales from the Accept-Language HTTP header.
8
+ #
9
+ # Returns locales sorted by quality value in descending order.
10
+ #
11
+ # @example
12
+ # detector = Header.new
13
+ # env = { "HTTP_ACCEPT_LANGUAGE" => "ja,en;q=0.9,de;q=0.8" }
14
+ # detector.call(env) # => ["ja", "en", "de"]
15
+ class Header
16
+ # @param env [Hash] Rack environment
17
+ # @return [Array<String>, nil] Locales sorted by quality value, or nil if header is missing
18
+ def call(env)
19
+ header = env["HTTP_ACCEPT_LANGUAGE"]
20
+ return nil if header.nil? || header.empty?
21
+
22
+ parse_accept_language(header)
23
+ end
24
+
25
+ private def parse_accept_language(header)
26
+ header.split(",")
27
+ .map {|part| parse_entry(part) }
28
+ .sort_by {|_, quality| -quality }
29
+ .map(&:first)
30
+ end
31
+
32
+ private def parse_entry(part)
33
+ locale, quality = part.strip.split(";q=")
34
+ [locale.strip, Float(quality || "1")]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ICU4X
5
+ class Locale
6
+ module Detector
7
+ # Detects locale from a query string parameter.
8
+ #
9
+ # @example With default parameter name
10
+ # detector = Query.new
11
+ # env = Rack::MockRequest.env_for("/?locale=ja")
12
+ # detector.call(env) # => "ja"
13
+ #
14
+ # @example With custom parameter name
15
+ # detector = Query.new("lang")
16
+ # env = Rack::MockRequest.env_for("/?lang=en")
17
+ # detector.call(env) # => "en"
18
+ class Query
19
+ DEFAULT_PARAM = "locale"
20
+ private_constant :DEFAULT_PARAM
21
+
22
+ # @param param [String] Query parameter name to read locale from
23
+ def initialize(param=DEFAULT_PARAM)
24
+ @param = param
25
+ end
26
+
27
+ # @param env [Hash] Rack environment
28
+ # @return [String, nil] Locale from query parameter, or nil if not present
29
+ def call(env)
30
+ query_string = env["QUERY_STRING"]
31
+ return nil if query_string.nil? || query_string.empty?
32
+
33
+ params = ::Rack::Utils.parse_query(query_string)
34
+ locale = params[@param]
35
+ return nil if locale.nil? || locale.empty?
36
+
37
+ locale
38
+ end
39
+
40
+ private attr_reader :param
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ICU4X
5
+ class Locale
6
+ # Factory module for building detector instances from various specification formats.
7
+ #
8
+ # Detectors are responsible for extracting locale preferences from the Rack environment.
9
+ # All detectors respond to `#call(env)` and return:
10
+ # - String: single locale (e.g., "ja")
11
+ # - Array<String>: multiple locales in preference order (e.g., ["ja", "en"])
12
+ # - nil: no locale detected
13
+ #
14
+ # @example Building detectors from different specification formats
15
+ # Detector.build(:header) # => Detector::Header.new
16
+ # Detector.build({ cookie: "locale" }) # => Detector::Cookie.new("locale")
17
+ # Detector.build(->(env) { "ja" }) # => the Proc itself
18
+ module Detector
19
+ class InvalidSpecificationError < Error; end
20
+
21
+ INFLECTOR = Zeitwerk::Inflector.new
22
+ private_constant :INFLECTOR
23
+
24
+ # Build a detector from various specification formats.
25
+ #
26
+ # @param spec [Symbol, Hash, Proc, #call] Detector specification
27
+ # @return [#call] Detector instance responding to #call(env)
28
+ # @raise [InvalidSpecificationError] if the specification is invalid
29
+ def self.build(spec)
30
+ case spec
31
+ when Symbol then build_from_symbol(spec)
32
+ when Hash then build_from_hash(spec)
33
+ when Proc then spec
34
+ else
35
+ validate_callable!(spec)
36
+ spec
37
+ end
38
+ end
39
+
40
+ private_class_method def self.build_from_symbol(symbol)
41
+ class_name = INFLECTOR.camelize(symbol.to_s, nil)
42
+ const_get(class_name).new
43
+ rescue NameError
44
+ raise InvalidSpecificationError, "Unknown detector: #{symbol.inspect}"
45
+ end
46
+
47
+ private_class_method def self.build_from_hash(hash)
48
+ raise InvalidSpecificationError, "Hash must have exactly one key" unless hash.size == 1
49
+
50
+ type, arg = hash.first
51
+ class_name = INFLECTOR.camelize(type.to_s, nil)
52
+ const_get(class_name).new(arg)
53
+ rescue NameError
54
+ raise InvalidSpecificationError, "Unknown detector type: #{type.inspect}"
55
+ end
56
+
57
+ private_class_method def self.validate_callable!(obj)
58
+ return if obj.respond_to?(:call)
59
+
60
+ raise InvalidSpecificationError, "Detector must respond to #call: #{obj.inspect}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ICU4X
5
+ class Locale
6
+ # Performs script-safe language negotiation using ICU4X's maximize.
7
+ #
8
+ # Matches requested locales against available locales, respecting script differences
9
+ # to avoid politically sensitive fallbacks (e.g., zh-TW won't match zh-CN).
10
+ #
11
+ # @example
12
+ # locales = %w[en-US en-GB ja].map { |s| ICU4X::Locale.parse(s) }
13
+ # negotiator = Rack::ICU4X::Locale::Negotiator.new(locales)
14
+ # negotiator.negotiate(%w[en-AU ja-JP]) # => ["en-US", "ja"]
15
+ class Negotiator
16
+ # @param available_locales [Array<ICU4X::Locale>] List of available locales
17
+ def initialize(available_locales)
18
+ @available = build_available_index(available_locales)
19
+ end
20
+
21
+ # Negotiate locales, returning all matches in preference order.
22
+ #
23
+ # @param requested_locales [Array<String>] Requested locale identifiers in preference order
24
+ # @yield [String] Invalid locale string that could not be parsed
25
+ # @return [Array<String>] Matched locale identifiers
26
+ def negotiate(requested_locales)
27
+ matched = []
28
+ remaining = @available.dup
29
+
30
+ requested_locales.each do |req_str|
31
+ req_max = maximize_locale(req_str)
32
+
33
+ # 1. Exact match (language + script + region)
34
+ if (found = find_exact_match(remaining, req_max))
35
+ matched << found[:original]
36
+ remaining.delete(found)
37
+ next
38
+ end
39
+
40
+ # 2. Language + Script match (ignore region)
41
+ # CRITICAL: Script MUST match to avoid politically sensitive fallbacks
42
+ if (found = find_lang_script_match(remaining, req_max))
43
+ matched << found[:original]
44
+ remaining.delete(found)
45
+ end
46
+ rescue
47
+ yield req_str if block_given?
48
+ end
49
+
50
+ matched
51
+ end
52
+
53
+ private def build_available_index(locales)
54
+ locales.map do |locale|
55
+ maximized = locale.maximize
56
+ {
57
+ original: locale.to_s,
58
+ locale:,
59
+ maximized:,
60
+ language: maximized.language,
61
+ script: maximized.script,
62
+ region: maximized.region
63
+ }
64
+ end
65
+ end
66
+
67
+ private def maximize_locale(locale_str) = ::ICU4X::Locale.parse(locale_str).maximize
68
+
69
+ private def find_exact_match(candidates, req_max)
70
+ candidates.find do |entry|
71
+ entry[:language] == req_max.language &&
72
+ entry[:script] == req_max.script &&
73
+ entry[:region] == req_max.region
74
+ end
75
+ end
76
+
77
+ private def find_lang_script_match(candidates, req_max)
78
+ candidates.find do |entry|
79
+ entry[:language] == req_max.language &&
80
+ entry[:script] == req_max.script
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ICU4X
5
+ class Locale
6
+ VERSION = "0.5.0"
7
+ public_constant :VERSION
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "icu4x"
4
+ require "rack"
5
+ require "zeitwerk"
6
+ require_relative "locale/version"
7
+
8
+ module Rack
9
+ module ICU4X
10
+ # Rack middleware that detects user's preferred locales from various sources.
11
+ #
12
+ # Uses ICU4X's maximize for script-safe language negotiation that respects
13
+ # script boundaries (e.g., zh-TW/Hant will NOT match zh-CN/Hans).
14
+ #
15
+ # @example Basic usage (Accept-Language header only)
16
+ # use Rack::ICU4X::Locale, locales: %w[en ja]
17
+ #
18
+ # @example With multiple detectors
19
+ # use Rack::ICU4X::Locale,
20
+ # locales: %w[en ja],
21
+ # detectors: [{ query: "lang" }, { cookie: "locale" }, :header]
22
+ #
23
+ # @example With custom detector
24
+ # use Rack::ICU4X::Locale,
25
+ # locales: %w[en ja],
26
+ # detectors: [->(env) { env["rack.session"]&.[]("locale") }, :header]
27
+ class Locale
28
+ Zeitwerk::Loader.new.tap do |loader|
29
+ loader.push_dir("#{__dir__}/locale", namespace: Rack::ICU4X::Locale)
30
+ loader.ignore("#{__dir__}/locale/version.rb")
31
+ loader.setup
32
+ end
33
+
34
+ ENV_KEY = "rack.icu4x.locale"
35
+ public_constant :ENV_KEY
36
+
37
+ DEFAULT_DETECTORS = [:header].freeze
38
+ public_constant :DEFAULT_DETECTORS
39
+
40
+ class Error < StandardError; end
41
+
42
+ # @param app [#call] The Rack application
43
+ # @param locales [Array<String, ICU4X::Locale>] List of available locales
44
+ # @param detectors [Array] Detector specifications (optional, default: [:header])
45
+ # @param default [String, ICU4X::Locale, nil] Default locale when no match is found (optional)
46
+ def initialize(app, locales:, detectors: DEFAULT_DETECTORS, default: nil)
47
+ @app = app
48
+ @locales = locales.map {|locale| normalize_locale(locale) }
49
+ @detectors = build_detectors(detectors)
50
+ @default = default && normalize_locale(default)
51
+ validate_default_in_locales!
52
+ @negotiator = Negotiator.new(@locales)
53
+ end
54
+
55
+ # @param env [Hash] Rack environment
56
+ # @return [Array] Response from the wrapped application
57
+ def call(env)
58
+ env[ENV_KEY] = detect_locales(env)
59
+ @app.call(env)
60
+ end
61
+
62
+ private def validate_default_in_locales!
63
+ return unless @default
64
+ return if @locales.include?(@default)
65
+
66
+ raise Error, "default #{@default.to_s.inspect} is not in available locales"
67
+ end
68
+
69
+ private def build_detectors(specs)
70
+ effective_specs = specs.empty? ? DEFAULT_DETECTORS : specs
71
+ effective_specs.map {|spec| Detector.build(spec) }
72
+ end
73
+
74
+ private def detect_locales(env)
75
+ result = try_detectors(env)
76
+ result.empty? && @default ? [@default] : result
77
+ end
78
+
79
+ private def try_detectors(env)
80
+ @detectors.each do |detector|
81
+ raw = detector.call(env)
82
+ next if raw.nil?
83
+
84
+ requested = Array(raw)
85
+ matched = @negotiator.negotiate(requested) {|invalid_locale|
86
+ log_invalid_locale(env, invalid_locale)
87
+ }
88
+ return matched.map {|locale| ::ICU4X::Locale.parse(locale) } unless matched.empty?
89
+ end
90
+ []
91
+ end
92
+
93
+ private def normalize_locale(locale) = locale.is_a?(::ICU4X::Locale) ? locale : ::ICU4X::Locale.parse(locale)
94
+
95
+ private def log_invalid_locale(env, locale)
96
+ message = "Ignored invalid locale: #{locale}"
97
+ if env["rack.logger"]
98
+ env["rack.logger"].warn(message)
99
+ else
100
+ env["rack.errors"]&.puts(message)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,20 @@
1
+ module Rack
2
+ module ICU4X
3
+ class Locale
4
+ module Detector
5
+ class Cookie
6
+ DEFAULT_NAME: String
7
+
8
+ @name: String
9
+
10
+ def initialize: (?String name) -> void
11
+ def call: (Hash[String, untyped] env) -> String?
12
+
13
+ private
14
+
15
+ attr_reader name: String
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Rack
2
+ module ICU4X
3
+ class Locale
4
+ module Detector
5
+ class Header
6
+ def initialize: () -> void
7
+ def call: (Hash[String, untyped] env) -> Array[String]?
8
+
9
+ private
10
+
11
+ def parse_accept_language: (String header) -> Array[String]
12
+ def parse_entry: (String part) -> [String, Float]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Rack
2
+ module ICU4X
3
+ class Locale
4
+ module Detector
5
+ class Query
6
+ DEFAULT_PARAM: String
7
+
8
+ @param: String
9
+
10
+ def initialize: (?String param) -> void
11
+ def call: (Hash[String, untyped] env) -> String?
12
+
13
+ private
14
+
15
+ attr_reader param: String
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module Rack
2
+ module ICU4X
3
+ class Locale
4
+ module Detector
5
+ class InvalidSpecificationError < Error
6
+ end
7
+
8
+ INFLECTOR: Zeitwerk::Inflector
9
+
10
+ type detector_result = String | Array[String] | nil
11
+ type detector_spec = Symbol | Hash[Symbol, String] | ^(Hash[String, untyped]) -> detector_result | _Detector
12
+
13
+ def self.build: (detector_spec spec) -> _Detector
14
+
15
+ private
16
+
17
+ def self.build_from_symbol: (Symbol symbol) -> _Detector
18
+ def self.build_from_hash: (Hash[Symbol, String] hash) -> _Detector
19
+ def self.validate_callable!: (untyped obj) -> void
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ interface _Detector
26
+ def call: (Hash[String, untyped] env) -> (String | Array[String] | nil)
27
+ end
@@ -0,0 +1,29 @@
1
+ module Rack
2
+ module ICU4X
3
+ class Locale
4
+ class Negotiator
5
+ type available_entry = {
6
+ original: String,
7
+ locale: ICU4X::Locale,
8
+ maximized: ICU4X::Locale,
9
+ language: String,
10
+ script: String,
11
+ region: String
12
+ }
13
+
14
+ @available: Array[available_entry]
15
+
16
+ def initialize: (Array[ICU4X::Locale] available_locales) -> void
17
+
18
+ def negotiate: (Array[String] requested_locales) ?{ (String) -> void } -> Array[String]
19
+
20
+ private
21
+
22
+ def build_available_index: (Array[ICU4X::Locale] locales) -> Array[available_entry]
23
+ def maximize_locale: (String locale_str) -> ICU4X::Locale
24
+ def find_exact_match: (Array[available_entry] candidates, ICU4X::Locale req_max) -> available_entry?
25
+ def find_lang_script_match: (Array[available_entry] candidates, ICU4X::Locale req_max) -> available_entry?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ module Rack
2
+ module ICU4X
3
+ class Locale
4
+ VERSION: String
5
+ ENV_KEY: String
6
+ DEFAULT_DETECTORS: Array[Symbol]
7
+
8
+ class Error < StandardError
9
+ end
10
+
11
+ type locale_input = String | ICU4X::Locale
12
+
13
+ @app: _RackApp
14
+ @locales: Array[ICU4X::Locale]
15
+ @detectors: Array[_Detector]
16
+ @default: ICU4X::Locale?
17
+ @negotiator: Negotiator
18
+
19
+ def initialize: (
20
+ _RackApp app,
21
+ locales: Array[locale_input],
22
+ ?detectors: Array[Detector::detector_spec],
23
+ ?default: locale_input?
24
+ ) -> void
25
+ def call: (Hash[String, untyped] env) -> [Integer, Hash[String, String], _ToA[String]]
26
+
27
+ private
28
+
29
+ def validate_default_in_locales!: () -> void
30
+ def build_detectors: (Array[Detector::detector_spec] specs) -> Array[_Detector]
31
+ def detect_locales: (Hash[String, untyped] env) -> Array[ICU4X::Locale]
32
+ def try_detectors: (Hash[String, untyped] env) -> Array[ICU4X::Locale]
33
+ def normalize_locale: (locale_input locale) -> ICU4X::Locale
34
+ def log_invalid_locale: (Hash[String, untyped] env, String locale) -> void
35
+ end
36
+ end
37
+ end
38
+
39
+ interface _RackApp
40
+ def call: (Hash[String, untyped] env) -> [Integer, Hash[String, String], _ToA[String]]
41
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-icu4x-locale
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - OZAWA Sakuro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-01-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: icu4x
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.7'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.7'
55
+ description: rack-icu4x-locale
56
+ email:
57
+ - 10973+sakuro@users.noreply.github.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/rack/icu4x/locale.rb
66
+ - lib/rack/icu4x/locale/detector.rb
67
+ - lib/rack/icu4x/locale/detector/cookie.rb
68
+ - lib/rack/icu4x/locale/detector/header.rb
69
+ - lib/rack/icu4x/locale/detector/query.rb
70
+ - lib/rack/icu4x/locale/negotiator.rb
71
+ - lib/rack/icu4x/locale/version.rb
72
+ - sig/rack/icu4x/locale.rbs
73
+ - sig/rack/icu4x/locale/detector.rbs
74
+ - sig/rack/icu4x/locale/detector/cookie.rbs
75
+ - sig/rack/icu4x/locale/detector/header.rbs
76
+ - sig/rack/icu4x/locale/detector/query.rbs
77
+ - sig/rack/icu4x/locale/negotiator.rbs
78
+ homepage: https://github.com/sakuro/rack-icu4x-locale
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/sakuro/rack-icu4x-locale
83
+ source_code_uri: https://github.com/sakuro/rack-icu4x-locale.git
84
+ changelog_uri: https://github.com/sakuro/rack-icu4x-locale/blob/main/CHANGELOG.md
85
+ rubygems_mfa_required: 'true'
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '3.2'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.4.19
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: rack-icu4x-locale
105
+ test_files: []