Updated the LocalizationMiddleware middleware in the Infrastructure package to read explicit language requests.
This commit is contained in:
@@ -5,11 +5,13 @@ import Localization
|
||||
|
||||
/// Resolves the visitor's preferred language and records it on the request context.
|
||||
///
|
||||
/// Placed ahead of the localized responders in the middleware chain, it reads the request's `Accept-Language` header, negotiates the best supported
|
||||
/// match (falling back to the default language), and stores it on the context's ``LocalizedRequestContext/language``.
|
||||
/// Placed ahead of the localized responders in the middleware chain, it reads the request's `lang` query parameter, its path, and its
|
||||
/// `Accept-Language` header, negotiates the best supported match (falling back to the default language), and stores it on the context's
|
||||
/// ``LocalizedRequestContext/language``.
|
||||
///
|
||||
/// The request is otherwise passed through untouched — the URL and routing are not affected — so each page is served at its existing path and varies its
|
||||
/// content by header.
|
||||
/// The query parameter is the deliberate override a language switcher links to; failing that, a leading path segment naming a supported language
|
||||
/// pins it, so an unrouted path under a language's prefix — its not-found page — answers in that language. Values naming no supported language
|
||||
/// are ignored, leaving the header. The request is passed through untouched — the path and routing are unaffected.
|
||||
public struct LocalizationMiddleware<Context: LocalizedRequestContext> {
|
||||
|
||||
// MARK: Properties
|
||||
@@ -49,7 +51,13 @@ extension LocalizationMiddleware: RouterMiddleware {
|
||||
) async throws -> Response {
|
||||
var context = context
|
||||
|
||||
// The deliberate query override first; failing that, the leading path segment, so a language's whole URL
|
||||
// prefix — routed or not — answers in its language.
|
||||
let requested = request.uri.queryParameters[.Parameter.language].map(String.init)
|
||||
?? request.uri.path.split(separator: "/").first.map(String.init)
|
||||
|
||||
context.language = negotiate(
|
||||
requested: requested,
|
||||
acceptLanguage: request.headers[.acceptLanguage]
|
||||
)
|
||||
|
||||
@@ -57,3 +65,13 @@ extension LocalizationMiddleware: RouterMiddleware {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Substring {
|
||||
/// A namespace for the query parameters the middleware reads.
|
||||
enum Parameter {
|
||||
/// The query parameter carrying an explicit language choice; the site's language switcher appends it to the current path.
|
||||
static let language: Substring = "lang"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public struct SocialCard: Sendable {
|
||||
|
||||
/// The locale of the card's text, or `nil` to omit its tag.
|
||||
///
|
||||
/// Open Graph specifies the `language_TERRITORY` form (e.g. `en_NL`); scrapers also accept a bare language code (e.g. `en`).
|
||||
/// Open Graph specifies the `language_TERRITORY` form (e.g. `en_US`); scrapers also accept a bare language code (e.g. `en`).
|
||||
public let locale: String?
|
||||
|
||||
/// The name of the site the card belongs to, or `nil` to omit its tag.
|
||||
@@ -41,7 +41,7 @@ public struct SocialCard: Sendable {
|
||||
/// - summary: the card's summary, or `nil` (the default) to omit its tag.
|
||||
/// - url: the absolute URL the card's page is served at, or `nil` (the default) to omit its tag.
|
||||
/// - siteName: the name of the site the card belongs to, or `nil` (the default) to omit its tag.
|
||||
/// - locale: the locale of the card's text, ideally in Open Graph's `language_TERRITORY` form (e.g. `en_NL`), or `nil` (the default)
|
||||
/// - locale: the locale of the card's text, ideally in Open Graph's `language_TERRITORY` form (e.g. `en_US`), or `nil` (the default)
|
||||
/// to omit its tag.
|
||||
/// - image: the card's share image, or `nil` (the default) to omit its tags.
|
||||
/// - type: the Open Graph type of the object the card describes. Defaults to `website`.
|
||||
|
||||
+43
@@ -25,6 +25,9 @@ struct LocalizationMiddlewareTests {
|
||||
router.get("language") { _, context in
|
||||
context.language
|
||||
}
|
||||
router.get("de/language") { _, context in
|
||||
context.language
|
||||
}
|
||||
|
||||
return router
|
||||
}())
|
||||
@@ -69,4 +72,44 @@ struct LocalizationMiddlewareTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `honours a supported lang query parameter over the header`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/language?lang=de",
|
||||
method: .get,
|
||||
headers: [.acceptLanguage: "en"]
|
||||
) { response in
|
||||
#expect(String(buffer: response.body) == "de")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A language's whole URL prefix answers in its language, so its not-found page does too.
|
||||
@Test
|
||||
func `pins the language a leading path segment names over the header`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/de/language",
|
||||
method: .get,
|
||||
headers: [.acceptLanguage: "en"]
|
||||
) { response in
|
||||
#expect(String(buffer: response.body) == "de")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `negotiates the header when the lang query parameter is unsupported`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/language?lang=fr",
|
||||
method: .get,
|
||||
headers: [.acceptLanguage: "de"]
|
||||
) { response in
|
||||
#expect(String(buffer: response.body) == "de")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user