utopia 2.32.0 → 3.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/utopia/environment.rb +6 -6
- data/bake/utopia/server.rb +4 -1
- data/bake/utopia/site.rb +14 -14
- data/bake/utopia/static.rb +26 -13
- data/bake/utopia.rb +1 -1
- data/context/getting-started.md +7 -5
- data/context/index.yaml +1 -2
- data/context/middleware.md +42 -13
- data/lib/utopia/application.rb +83 -0
- data/lib/utopia/content/builder.rb +25 -0
- data/lib/utopia/content/document.rb +64 -6
- data/lib/utopia/content/link.rb +50 -5
- data/lib/utopia/content/links.rb +44 -7
- data/lib/utopia/content/markup.rb +74 -0
- data/lib/utopia/content/middleware.rb +48 -20
- data/lib/utopia/content/namespace.rb +10 -1
- data/lib/utopia/content/node.rb +66 -4
- data/lib/utopia/content/response.rb +12 -3
- data/lib/utopia/content.rb +3 -0
- data/lib/utopia/controller/actions.md +4 -4
- data/lib/utopia/controller/actions.rb +51 -3
- data/lib/utopia/controller/base.rb +72 -29
- data/lib/utopia/controller/middleware.rb +20 -15
- data/lib/utopia/controller/respond.rb +36 -32
- data/lib/utopia/controller/responder.rb +106 -45
- data/lib/utopia/controller/result.rb +11 -0
- data/lib/utopia/controller/rewrite.rb +39 -0
- data/lib/utopia/controller/variables.rb +15 -3
- data/lib/utopia/controller.rb +2 -0
- data/lib/utopia/exceptions/handler.rb +20 -11
- data/lib/utopia/exceptions/mailer.rb +56 -51
- data/lib/utopia/extensions/array_split.rb +6 -0
- data/lib/utopia/extensions/date_comparisons.rb +6 -0
- data/lib/utopia/http.rb +11 -48
- data/lib/utopia/import_map.rb +19 -11
- data/lib/utopia/localization/locales.rb +60 -0
- data/lib/utopia/localization/middleware.rb +107 -73
- data/lib/utopia/localization/preferences.rb +76 -0
- data/lib/utopia/localization/resolver.rb +47 -0
- data/lib/utopia/localization.rb +6 -0
- data/lib/utopia/middleware.rb +3 -4
- data/lib/utopia/path/matcher.rb +15 -0
- data/lib/utopia/path.rb +146 -10
- data/lib/utopia/redirection/client_redirect.rb +87 -0
- data/lib/utopia/redirection/directory_index.rb +41 -0
- data/lib/utopia/redirection/errors.rb +78 -0
- data/lib/utopia/redirection/moved.rb +52 -0
- data/lib/utopia/redirection/request_failure.rb +26 -0
- data/lib/utopia/redirection/rewrite.rb +42 -0
- data/lib/utopia/redirection.rb +7 -165
- data/lib/utopia/request.rb +202 -0
- data/lib/utopia/response.rb +73 -0
- data/lib/utopia/session/lazy_hash.rb +27 -1
- data/lib/utopia/session/middleware.rb +103 -30
- data/lib/utopia/session/serialization.rb +8 -0
- data/lib/utopia/session.rb +5 -0
- data/lib/utopia/setup.rb +23 -3
- data/lib/utopia/shell.rb +28 -7
- data/lib/utopia/static/local_file.rb +84 -61
- data/lib/utopia/static/middleware.rb +65 -33
- data/lib/utopia/static/mime_types.rb +38 -29
- data/lib/utopia/static.rb +2 -0
- data/lib/utopia/version.rb +3 -2
- data/lib/utopia.rb +1 -1
- data/license.md +1 -1
- data/readme.md +33 -4
- data/releases.md +15 -0
- data/setup/site/bake.rb +1 -1
- data/setup/site/config/application.rb +51 -0
- data/setup/site/config/serve.rb +8 -0
- data/setup/site/falcon.rb +17 -4
- data/setup/site/fixtures/website.rb +27 -11
- data/setup/site/gems.rb +1 -3
- data/setup/site/lib/readme.txt +1 -1
- data/setup/site/pages/welcome/index.xnode +3 -3
- data/setup/site/readme.md +0 -3
- data/setup/site/test/website.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +60 -34
- metadata.gz.sig +0 -0
- data/lib/utopia/localization/wrapper.rb +0 -52
- data/setup/site/Guardfile +0 -12
- data/setup/site/config.ru +0 -49
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Utopia
|
|
7
|
+
module Localization
|
|
8
|
+
# Matches configured locales against language ranges.
|
|
9
|
+
class Locales
|
|
10
|
+
# Expand a locale into progressively less specific language ranges.
|
|
11
|
+
# @parameter locale [String] The locale to expand.
|
|
12
|
+
# @parameter patterns [Hash] The destination language-range mapping.
|
|
13
|
+
def self.expand(locale, patterns)
|
|
14
|
+
parts = locale.split("-")
|
|
15
|
+
|
|
16
|
+
while parts.any?
|
|
17
|
+
pattern = parts.join("-")
|
|
18
|
+
patterns[pattern] ||= locale
|
|
19
|
+
parts.pop
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Initialize the configured locales.
|
|
24
|
+
# @parameter names [Array(String)] The locale names, in preference order.
|
|
25
|
+
def initialize(names)
|
|
26
|
+
@names = names
|
|
27
|
+
@patterns = {}
|
|
28
|
+
|
|
29
|
+
@names.each do |name|
|
|
30
|
+
self.class.expand(name, @patterns)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Freeze this object and its internal state.
|
|
37
|
+
# @returns [self] This object.
|
|
38
|
+
def freeze
|
|
39
|
+
return self if frozen?
|
|
40
|
+
|
|
41
|
+
@names.freeze
|
|
42
|
+
@patterns.freeze
|
|
43
|
+
|
|
44
|
+
return super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr :names
|
|
48
|
+
attr :patterns
|
|
49
|
+
|
|
50
|
+
# Select configured locales matching the given language ranges.
|
|
51
|
+
# @parameter languages [Enumerable] Preferred language ranges.
|
|
52
|
+
# @returns [Array(String)] Matching locale names in language preference order.
|
|
53
|
+
def match(languages)
|
|
54
|
+
languages.filter_map do |language|
|
|
55
|
+
@patterns[language.name]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "preferences"
|
|
7
|
+
require_relative "locales"
|
|
8
|
+
require_relative "../middleware"
|
|
9
|
+
require_relative "../request"
|
|
10
|
+
require_relative "../response"
|
|
11
|
+
|
|
12
|
+
require "set"
|
|
13
|
+
require "protocol/http/header/accept_language"
|
|
7
14
|
|
|
8
15
|
module Utopia
|
|
9
16
|
module Localization
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
HTTP_ACCEPT_LANGUAGE = "HTTP_ACCEPT_LANGUAGE".freeze
|
|
14
|
-
|
|
17
|
+
# Computes localization preferences and rewrites locale-prefixed paths.
|
|
18
|
+
class Middleware < Protocol::HTTP::Middleware
|
|
15
19
|
# @param locales [Array<String>] An array of all supported locales.
|
|
16
20
|
# @param default_locale [String] The default locale if none is provided.
|
|
17
|
-
# @param default_locales [String] The locales to try in order if none is provided.
|
|
18
|
-
# @param hosts [Hash<Pattern, String>] Specify a mapping of
|
|
19
|
-
# @param ignore [Array<Pattern>] A list of patterns matched against
|
|
21
|
+
# @param default_locales [Array<String | Nil>] The locales to try in order if none is provided.
|
|
22
|
+
# @param hosts [Hash<Pattern, String>] Specify a mapping of request hosts to locales.
|
|
23
|
+
# @param ignore [Array<Pattern>] A list of patterns matched against request paths which will not be localized.
|
|
20
24
|
def initialize(app, locales:, default_locale: nil, default_locales: nil, hosts: {}, ignore: [])
|
|
21
|
-
|
|
25
|
+
super(app)
|
|
22
26
|
|
|
23
|
-
@all_locales =
|
|
27
|
+
@all_locales = Locales.new(locales)
|
|
24
28
|
|
|
25
29
|
# Locales here are represented as an array of strings, e.g. ['en', 'ja', 'cn', 'de'] and are used in order if no locale is specified by the user.
|
|
26
30
|
unless @default_locales = default_locales
|
|
@@ -41,11 +45,11 @@ module Utopia
|
|
|
41
45
|
# Select a localization based on a request host name:
|
|
42
46
|
@hosts = hosts
|
|
43
47
|
|
|
44
|
-
@ignore = ignore
|
|
45
|
-
|
|
46
|
-
@methods = methods
|
|
48
|
+
@ignore = ignore
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
# Freeze this object and its internal state.
|
|
52
|
+
# @returns [self] This object.
|
|
49
53
|
def freeze
|
|
50
54
|
return self if frozen?
|
|
51
55
|
|
|
@@ -61,112 +65,142 @@ module Utopia
|
|
|
61
65
|
attr :all_locales
|
|
62
66
|
attr :default_locale
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
# Compute the preferred locales for a request.
|
|
69
|
+
# @parameter request [Utopia::Request] The application request.
|
|
70
|
+
# @parameter path_locale [String | Nil] The locale extracted from the request path.
|
|
71
|
+
# @returns [Array(String | Nil)] The unique locales in preference order.
|
|
72
|
+
def preferred_locales(request, path_locale = nil)
|
|
67
73
|
# Keep track of what locales have been tried:
|
|
68
74
|
locales = Set.new
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
if path_locale
|
|
77
|
+
locales.add(path_locale)
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
env = env.merge(Rack::PATH_INFO => path.to_s)
|
|
77
|
-
|
|
78
|
-
yield env.merge(CURRENT_LOCALE_KEY => locale) if locales.add? locale
|
|
80
|
+
host_preferred_locales(request) do |locale|
|
|
81
|
+
locales.add(locale)
|
|
79
82
|
end
|
|
80
83
|
|
|
81
|
-
browser_preferred_locales(
|
|
82
|
-
|
|
84
|
+
browser_preferred_locales(request).each do |locale|
|
|
85
|
+
locales.add(locale)
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
@default_locales.each do |locale|
|
|
86
|
-
|
|
89
|
+
locales.add(locale)
|
|
87
90
|
end
|
|
91
|
+
|
|
92
|
+
return locales.to_a
|
|
88
93
|
end
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
# Infer preferred locales from the request authority.
|
|
96
|
+
# @parameter request [Utopia::Request] The application request.
|
|
97
|
+
# @yields {|locale| ...} Each locale whose host pattern matches the request authority.
|
|
98
|
+
# @returns [Hash] The configured host mappings.
|
|
99
|
+
def host_preferred_locales(request)
|
|
100
|
+
authority = request.authority.to_s
|
|
101
|
+
|
|
102
|
+
# Yield all hosts which match the incoming authority:
|
|
94
103
|
@hosts.each do |pattern, locale|
|
|
95
|
-
|
|
104
|
+
if authority[pattern]
|
|
105
|
+
yield locale
|
|
106
|
+
end
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
# Extract a locale prefix from the request path.
|
|
111
|
+
# @parameter request [Utopia::Request] The application request.
|
|
112
|
+
# @returns [Array(Utopia::Request, String | Nil)] The request and extracted locale.
|
|
113
|
+
def extract_path_locale(request)
|
|
114
|
+
path = request.url.path
|
|
115
|
+
|
|
116
|
+
# Localization prefixes only apply to absolute application paths:
|
|
117
|
+
unless path.absolute?
|
|
118
|
+
return request, nil
|
|
119
|
+
end
|
|
101
120
|
|
|
102
|
-
if
|
|
103
|
-
#
|
|
104
|
-
|
|
121
|
+
if segment = path.segments[1]
|
|
122
|
+
# Decode only the component which may contain the locale:
|
|
123
|
+
component = Protocol::URL::Encoding::System.unescape(segment)
|
|
105
124
|
|
|
106
|
-
|
|
125
|
+
if request_locale = @all_locales.patterns[component]
|
|
126
|
+
# Remove the locale while preserving all other encoded segments:
|
|
127
|
+
segments = path.segments.dup
|
|
128
|
+
segments.delete_at(1)
|
|
129
|
+
|
|
130
|
+
# Preserve the absolute root when the locale was the only component:
|
|
131
|
+
if segments == [""]
|
|
132
|
+
segments << ""
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
path = Protocol::URL::Path.new(nil, segments)
|
|
136
|
+
|
|
137
|
+
return request.with(path: path), request_locale
|
|
138
|
+
end
|
|
107
139
|
end
|
|
140
|
+
|
|
141
|
+
return request, nil
|
|
108
142
|
end
|
|
109
143
|
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
# Parse the locales preferred by the browser.
|
|
145
|
+
# @parameter request [Utopia::Request] The application request.
|
|
146
|
+
# @returns [Array(String)] Supported locales accepted by the browser, in preference order.
|
|
147
|
+
def browser_preferred_locales(request)
|
|
148
|
+
accept_languages = request.headers["accept-language"]
|
|
112
149
|
|
|
113
150
|
# No user prefered languages:
|
|
114
151
|
return [] unless accept_languages
|
|
115
152
|
|
|
116
153
|
# Extract the ordered list of languages:
|
|
117
|
-
languages =
|
|
154
|
+
languages = accept_languages.preferred_languages
|
|
118
155
|
|
|
119
156
|
# Returns available languages based on the order languages:
|
|
120
|
-
return @all_locales
|
|
121
|
-
rescue HTTP::
|
|
157
|
+
return @all_locales.match(languages)
|
|
158
|
+
rescue Protocol::HTTP::Header::AcceptLanguage::ParseError
|
|
122
159
|
# If we fail to parse the browser Accept-Language header, we ignore it (silently).
|
|
123
160
|
return []
|
|
124
161
|
end
|
|
125
162
|
|
|
126
|
-
|
|
163
|
+
# Check whether the request path includes a locale.
|
|
164
|
+
# @parameter request [Utopia::Request] The application request.
|
|
165
|
+
# @returns [Boolean] Whether the path is eligible for localization.
|
|
166
|
+
def localized?(request)
|
|
127
167
|
# Ignore requests which match the ignored paths:
|
|
128
|
-
|
|
129
|
-
return false if @ignore.any?{|pattern|
|
|
168
|
+
path = request.url.path.encoded
|
|
169
|
+
return false if @ignore.any?{|pattern| path[pattern] != nil}
|
|
130
170
|
|
|
131
171
|
return true
|
|
132
172
|
end
|
|
133
173
|
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
# Mark the response as varying by language.
|
|
175
|
+
# @parameter response [Protocol::HTTP::Response] The response.
|
|
176
|
+
# @returns [Protocol::HTTP::Response] The response with localization headers.
|
|
177
|
+
def vary(response)
|
|
178
|
+
response = Response.wrap(response)
|
|
179
|
+
headers = response.headers
|
|
137
180
|
|
|
138
181
|
# This response was based on the Accept-Language header:
|
|
139
|
-
headers
|
|
140
|
-
|
|
141
|
-
# Althought this header is generally not supported, we supply it anyway as it is useful for debugging:
|
|
142
|
-
if locale = env[CURRENT_LOCALE_KEY]
|
|
143
|
-
# Set the Content-Location to point to the localized URI as requested:
|
|
144
|
-
headers["Content-Location"] = "/#{locale}" + env[Rack::PATH_INFO]
|
|
145
|
-
end
|
|
182
|
+
headers.add("vary", "Accept-Language")
|
|
146
183
|
|
|
147
184
|
return response
|
|
148
185
|
end
|
|
149
186
|
|
|
150
|
-
|
|
187
|
+
# Attach localization preferences and invoke the application once.
|
|
188
|
+
# @parameter request [Utopia::Request] The request.
|
|
189
|
+
# @returns [Protocol::HTTP::Response] The response with cache-variation headers.
|
|
190
|
+
def call(request)
|
|
151
191
|
# Pass the request through if it shouldn't be localized:
|
|
152
|
-
return @
|
|
153
|
-
|
|
154
|
-
env[LOCALIZATION_KEY] = self
|
|
192
|
+
return @delegate.call(request) unless localized?(request)
|
|
155
193
|
|
|
156
|
-
|
|
194
|
+
request, path_locale = extract_path_locale(request)
|
|
195
|
+
locales = preferred_locales(request, path_locale)
|
|
157
196
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
break unless response[0] >= 400
|
|
165
|
-
|
|
166
|
-
response[2].close if response[2].respond_to?(:close)
|
|
167
|
-
end
|
|
197
|
+
request.localization = Preferences.new(
|
|
198
|
+
all_locales: @all_locales.names,
|
|
199
|
+
preferred_locales: locales,
|
|
200
|
+
default_locale: @default_locale,
|
|
201
|
+
)
|
|
168
202
|
|
|
169
|
-
return vary(
|
|
203
|
+
return vary(@delegate.call(request))
|
|
170
204
|
end
|
|
171
205
|
end
|
|
172
206
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Utopia
|
|
7
|
+
module Localization
|
|
8
|
+
# Immutable localization preferences for a request or resolved resource.
|
|
9
|
+
class Preferences
|
|
10
|
+
# Initialize localization preferences.
|
|
11
|
+
# @parameter all_locales [Array(String)] All configured locales.
|
|
12
|
+
# @parameter preferred_locales [Array(String | Nil)] Locales in resolution order.
|
|
13
|
+
# @parameter default_locale [String | Nil] The configured default locale.
|
|
14
|
+
# @parameter locale [String | Nil] The currently selected locale.
|
|
15
|
+
def initialize(all_locales:, preferred_locales:, default_locale:, locale: preferred_locales.first)
|
|
16
|
+
@all_locales = all_locales
|
|
17
|
+
@preferred_locales = preferred_locales
|
|
18
|
+
@default_locale = default_locale
|
|
19
|
+
@locale = locale
|
|
20
|
+
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Freeze this object and its internal state.
|
|
25
|
+
# @returns [self] This object.
|
|
26
|
+
def freeze
|
|
27
|
+
return self if frozen?
|
|
28
|
+
|
|
29
|
+
@all_locales.each(&:freeze)
|
|
30
|
+
@preferred_locales.each(&:freeze)
|
|
31
|
+
@default_locale.freeze
|
|
32
|
+
@locale.freeze
|
|
33
|
+
|
|
34
|
+
@all_locales.freeze
|
|
35
|
+
@preferred_locales.freeze
|
|
36
|
+
|
|
37
|
+
return super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr :all_locales
|
|
41
|
+
attr :preferred_locales
|
|
42
|
+
attr :default_locale
|
|
43
|
+
attr :locale
|
|
44
|
+
|
|
45
|
+
# Whether localization is active.
|
|
46
|
+
# @returns [Boolean] `true` when locales are configured.
|
|
47
|
+
def localized?
|
|
48
|
+
!@all_locales.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Build a path with the given locale prefix.
|
|
52
|
+
# @parameter path [Utopia::Path | String] The path.
|
|
53
|
+
# @parameter locale [String | Nil] The locale.
|
|
54
|
+
# @returns [String] The locale-prefixed path.
|
|
55
|
+
def localized_path(path, locale = @locale)
|
|
56
|
+
if locale
|
|
57
|
+
return "/#{locale}#{path}"
|
|
58
|
+
else
|
|
59
|
+
return path.to_s
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Select a locale without changing the original preferences.
|
|
64
|
+
# @parameter locale [String | Nil] The selected locale.
|
|
65
|
+
# @returns [Preferences] A new localization preference object.
|
|
66
|
+
def with(locale:)
|
|
67
|
+
self.class.new(
|
|
68
|
+
all_locales: @all_locales,
|
|
69
|
+
preferred_locales: @preferred_locales,
|
|
70
|
+
default_locale: @default_locale,
|
|
71
|
+
locale: locale,
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../response"
|
|
7
|
+
|
|
8
|
+
module Utopia
|
|
9
|
+
module Localization
|
|
10
|
+
# Resolves localized resources from immutable request preferences.
|
|
11
|
+
module Resolver
|
|
12
|
+
CONTENT_LANGUAGE = "content-language".freeze
|
|
13
|
+
CONTENT_LOCATION = "content-location".freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Resolve a response by trying each preferred locale in order.
|
|
18
|
+
def resolve_localized(request)
|
|
19
|
+
if localization = request.localization
|
|
20
|
+
localization.preferred_locales.each do |locale|
|
|
21
|
+
selected = localization.with(locale: locale)
|
|
22
|
+
|
|
23
|
+
if response = yield(selected)
|
|
24
|
+
return localized_response(request, response, selected)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
return yield(nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Describe the selected localized representation in its response metadata.
|
|
35
|
+
def localized_response(request, response, localization)
|
|
36
|
+
response = Response.wrap(response)
|
|
37
|
+
|
|
38
|
+
if locale = localization.locale
|
|
39
|
+
response.headers[CONTENT_LANGUAGE] = locale
|
|
40
|
+
response.headers[CONTENT_LOCATION] = localization.localized_path(request.url.path.encoded)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return response
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/utopia/localization.rb
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2009-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require_relative "localization/preferences"
|
|
7
|
+
require_relative "localization/locales"
|
|
8
|
+
require_relative "localization/resolver"
|
|
6
9
|
require_relative "localization/middleware"
|
|
7
10
|
|
|
8
11
|
module Utopia
|
|
12
|
+
# Computes request localization preferences and resolves localized resources.
|
|
9
13
|
module Localization
|
|
14
|
+
# Construct localization middleware.
|
|
15
|
+
# @returns [Localization::Middleware] The localization middleware.
|
|
10
16
|
def self.new(...)
|
|
11
17
|
Middleware.new(...)
|
|
12
18
|
end
|
data/lib/utopia/middleware.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2009-
|
|
4
|
+
# Copyright, 2009-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http/middleware"
|
|
5
7
|
|
|
6
8
|
require_relative "http"
|
|
7
9
|
require_relative "path"
|
|
@@ -10,9 +12,6 @@ module Utopia
|
|
|
10
12
|
# The default pages path for {Utopia::Content} middleware.
|
|
11
13
|
PAGES_PATH = "pages".freeze
|
|
12
14
|
|
|
13
|
-
# This is used for shared controller variables which get consumed by the content middleware.
|
|
14
|
-
VARIABLES_KEY = "utopia.variables".freeze
|
|
15
|
-
|
|
16
15
|
# The default root directory for middleware to operate within, e.g. the web-site directory. Convention over configuration.
|
|
17
16
|
# @param subdirectory [String] Appended to the default root to make a more specific path.
|
|
18
17
|
# @param pwd [String] The working directory for the current site.
|
data/lib/utopia/path/matcher.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Utopia
|
|
|
15
15
|
class Matcher
|
|
16
16
|
# The result of matching against a {Path}.
|
|
17
17
|
class MatchData
|
|
18
|
+
# Initialize the named captures and unmatched path suffix.
|
|
19
|
+
# @parameter named_parts [Hash] The named match captures.
|
|
20
|
+
# @parameter post_match [String] The unmatched path suffix.
|
|
18
21
|
def initialize(named_parts, post_match)
|
|
19
22
|
@named_parts = named_parts
|
|
20
23
|
@post_match = Path[post_match]
|
|
@@ -26,10 +29,15 @@ module Utopia
|
|
|
26
29
|
# Any remaining part past the end of the explicitly matched components.
|
|
27
30
|
attr :post_match
|
|
28
31
|
|
|
32
|
+
# Fetch a named capture.
|
|
33
|
+
# @parameter key [String | Symbol] The lookup key.
|
|
34
|
+
# @returns [Object | Nil] The named captured value.
|
|
29
35
|
def [] key
|
|
30
36
|
@named_parts[key]
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
# Return the named captures.
|
|
40
|
+
# @returns [Array(String)] The capture names.
|
|
33
41
|
def names
|
|
34
42
|
@named_parts.keys
|
|
35
43
|
end
|
|
@@ -40,10 +48,17 @@ module Utopia
|
|
|
40
48
|
@patterns = patterns
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
# Construct a matcher from typed named patterns.
|
|
52
|
+
# @parameter patterns [Hash] The path rewrite patterns.
|
|
53
|
+
# @returns [Matcher] The matcher.
|
|
43
54
|
def self.[](patterns)
|
|
44
55
|
self.new(patterns)
|
|
45
56
|
end
|
|
46
57
|
|
|
58
|
+
# Coerce.
|
|
59
|
+
# @parameter klass [Class] The class to configure.
|
|
60
|
+
# @parameter value [String] The captured text.
|
|
61
|
+
# @returns [Object] The captured text coerced to `klass`.
|
|
47
62
|
def coerce(klass, value)
|
|
48
63
|
if klass == Integer
|
|
49
64
|
Integer(value)
|