Skip to content

Commit cc9bb1e

Browse files
Revise localization resolution.
Assisted-By: devx/b3414b50-d642-461b-95a8-8f773c4d087b
1 parent d1a22eb commit cc9bb1e

21 files changed

Lines changed: 317 additions & 214 deletions

File tree

context/middleware.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use Utopia::Redirection::Errors,
3636

3737
## Localization
3838

39-
The {ruby Utopia::Localization} middleware provides non-intrusive localization on top of the controller and view layers. The middleware uses the `accept-language` header to guess the preferred locale out of the given options. If a request path maps to a resource, that resource is returned. Otherwise, a non-localized request is made.
39+
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.
4040

4141
~~~ ruby
4242
use Utopia::Localization,
@@ -53,7 +53,7 @@ pages/index.ja.xnode
5353
pages/index.zh.xnode
5454
~~~
5555

56-
You can also access the current locale in the view via {ruby Utopia::Content::Node::Context#localization}.
56+
You can access the selected locale in a view using `localization.locale`. Controllers can inspect the request preferences using `request.localization`.
5757

5858
## Controller
5959

guides/middleware/readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use Utopia::Redirection::Errors,
3636

3737
## Localization
3838

39-
The {ruby Utopia::Localization} middleware provides non-intrusive localization on top of the controller and view layers. The middleware uses the `accept-language` header to guess the preferred locale out of the given options. If a request path maps to a resource, that resource is returned. Otherwise, a non-localized request is made.
39+
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.
4040

4141
~~~ ruby
4242
use Utopia::Localization,
@@ -53,7 +53,7 @@ pages/index.ja.xnode
5353
pages/index.zh.xnode
5454
~~~
5555

56-
You can also access the current locale in the view via {ruby Utopia::Content::Node::Context#localization}.
56+
You can access the selected locale in a view using `localization.locale`. Controllers can inspect the request preferences using `request.localization`.
5757

5858
## Controller
5959

lib/utopia/content/document.rb

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,19 @@ class Document < Response
3030
# @parameter node [Utopia::Content::Node] The content node.
3131
# @parameter request [Utopia::Request] The application request.
3232
# @parameter attributes [Hash] The attributes.
33+
# @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
3334
# @returns [Document] The rendered document.
34-
def self.render(node, request, attributes)
35-
self.new(request, attributes).render!(node, attributes)
35+
def self.render(node, request, attributes, localization: request&.localization)
36+
self.new(request, attributes, localization: localization).render!(node, attributes)
3637
end
3738

3839
# Initialize a document for a protocol request.
3940
# @parameter request [Utopia::Request] The application request.
4041
# @parameter attributes [Hash] The attributes.
41-
def initialize(request, attributes = {})
42+
# @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
43+
def initialize(request, attributes = {}, localization: request&.localization)
4244
@request = request
45+
@localization = localization
4346

4447
@attributes = attributes
4548

@@ -101,10 +104,10 @@ def controller
101104
@controller ||= Utopia::Controller[request]
102105
end
103106

104-
# Return a localization wrapper for the current request.
105-
# @returns [Localization::Wrapper] The localization wrapper.
107+
# Return the selected localization preferences for this document.
108+
# @returns [Localization::Preferences | Nil] The localization preferences.
106109
def localization
107-
@localization ||= Utopia::Localization[request]
110+
@localization
108111
end
109112

110113
# Parse markup into this document.

lib/utopia/content/links.rb

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ class Links
1919
# @parameter root [String] The root directory.
2020
# @parameter path [Utopia::Path | String] The path.
2121
# @parameter locale [String] The locale.
22+
# @parameter fallback [Boolean] Whether to return an unlocalized link as a fallback.
2223
# @returns [Link | Nil] The resolved link.
23-
def self.for(root, path, locale = nil)
24+
def self.for(root, path, locale = nil, fallback: true)
2425
warn "Using uncached links metadata!"
25-
self.new(root).for(path, locale)
26+
self.new(root).for(path, locale, fallback: fallback)
2627
end
2728

2829
# Build an index of links for the given path.
@@ -54,9 +55,12 @@ def initialize(root, extension: XNODE_EXTENSION)
5455
attr :index_filter
5556

5657
# Resolve a link for the specified path, which must be a path to a specific link.
57-
# for(Path["/index"])
58-
def for(path, locale = nil)
59-
links(path.dirname).lookup(path.last, locale)
58+
# @parameter path [Utopia::Path | String] The path.
59+
# @parameter locale [String | Nil] The locale.
60+
# @parameter fallback [Boolean] Whether to return an unlocalized link as a fallback.
61+
# @returns [Link | Nil] The resolved link.
62+
def for(path, locale = nil, fallback: true)
63+
links(path.dirname).lookup(path.last, locale, fallback: fallback)
6064
end
6165

6266
# Give an index of all links that can be reached from the given path.
@@ -208,16 +212,17 @@ def each(locale)
208212
# Lookup.
209213
# @parameter name [String] The name.
210214
# @parameter locale [String] The locale.
211-
# @returns [Link | Nil] The exact locale match, or the unlocalized link as a fallback.
212-
def lookup(name, locale = nil)
215+
# @parameter fallback [Boolean] Whether to return an unlocalized link as a fallback.
216+
# @returns [Link | Nil] The exact locale match, or the unlocalized link when fallback is enabled.
217+
def lookup(name, locale = nil, fallback: true)
213218
# This allows generic links to serve any locale requested.
214219
if links = @named[name]
215220
generic_link = nil
216221

217222
links.each do |link|
218223
if link.locale == locale
219224
return link
220-
elsif link.locale.nil?
225+
elsif fallback && link.locale.nil?
221226
generic_link = link
222227
end
223228
end

lib/utopia/content/middleware.rb

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Copyright, 2025-2026, by Samuel Williams.
55

66
require_relative "../middleware"
7-
require_relative "../localization"
7+
require_relative "../localization/resolver"
88
require_relative "../request"
99
require_relative "../response"
1010
require_relative "../controller/variables"
@@ -22,6 +22,8 @@ module Utopia
2222
module Content
2323
# A middleware which serves dynamically generated content based on markup files.
2424
class Middleware < Protocol::HTTP::Middleware
25+
include Localization::Resolver
26+
2527
CONTENT_NAMESPACE = "content".freeze
2628
UTOPIA_NAMESPACE = "utopia".freeze
2729
CONTENT_TAG_NAME = "utopia:content".freeze
@@ -105,12 +107,13 @@ def resolve_link(link)
105107
# Respond.
106108
# @parameter link [Utopia::Content::Link] The content link.
107109
# @parameter request [Utopia::Request] The application request.
110+
# @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
108111
# @returns [Protocol::HTTP::Response] The response.
109-
def respond(link, request)
112+
def respond(link, request, localization: request.localization)
110113
if node = resolve_link(link)
111114
attributes = request.variables&.to_hash || {}
112115

113-
return node.process!(request, attributes)
116+
return node.process!(request, attributes, localization: localization)
114117
elsif redirect_uri = link[:uri]
115118
return Utopia::Response[307, {HTTP::LOCATION => redirect_uri}, []]
116119
end
@@ -133,13 +136,18 @@ def call(request)
133136
return Utopia::Response[307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []]
134137
end
135138

136-
locale = request.locale
137-
if link = @links.for(path, locale)
138-
if response = self.respond(link, request)
139-
return response
139+
response = resolve_localized(request) do |localization|
140+
locale = localization&.locale
141+
142+
if link = @links.for(path, locale, fallback: false)
143+
self.respond(link, request, localization: localization)
140144
end
141145
end
142146

147+
if response
148+
return response
149+
end
150+
143151
return @delegate.call(request)
144152
end
145153

@@ -200,10 +208,11 @@ def content_tag(name, node)
200208
end
201209

202210
Traces::Provider(Middleware) do
203-
def respond(link, request)
211+
def respond(link, request, localization: request.localization)
204212
attributes = {
205213
"link.key" => link.key,
206-
"link.href" => link.href
214+
"link.href" => link.href,
215+
"link.locale" => localization&.locale,
207216
}
208217

209218
Traces.trace("utopia.content.middleware.respond", attributes: attributes){super}

lib/utopia/content/node.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ def call(document, state)
138138
# Process the request and return the resulting response.
139139
# @parameter request [Utopia::Request] The application request.
140140
# @parameter attributes [Hash] The attributes.
141+
# @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
141142
# @returns [Protocol::HTTP::Response] The response.
142-
def process!(request, attributes = {})
143-
Document.render(self, request, attributes).to_response
143+
def process!(request, attributes = {}, localization: request&.localization)
144+
Document.render(self, request, attributes, localization: localization).to_response
144145
end
145146

146147
# This is a special context in which a limited set of well defined methods are exposed in the content view.
@@ -167,8 +168,8 @@ def controller
167168
document.controller
168169
end
169170

170-
# Return the document's localization wrapper.
171-
# @returns [Localization::Wrapper] The localization wrapper.
171+
# Return the document's localization preferences.
172+
# @returns [Localization::Preferences | Nil] The localization preferences.
172173
def localization
173174
document.localization
174175
end

lib/utopia/exceptions/mailer.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ def current_state(request)
179179
session: request.session,
180180
variables: request.variables,
181181
localization: request.localization,
182-
current_locale: request.locale,
183182
exception: request.exception,
184183
}
185184
end

lib/utopia/localization.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
# Released under the MIT License.
44
# Copyright, 2009-2025, by Samuel Williams.
55

6+
require_relative "localization/preferences"
7+
require_relative "localization/resolver"
68
require_relative "localization/middleware"
79

810
module Utopia
11+
# Computes request localization preferences and resolves localized resources.
912
module Localization
1013
# Construct localization middleware.
1114
# @returns [Localization::Middleware] The localization middleware.

0 commit comments

Comments
 (0)