import Foundation import HTTPTypes import Hummingbird 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 `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 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 { // MARK: Properties /// Negotiates the request's language from its `Accept-Language` header. private let negotiate: Negotiate // MARK: Initializers /// Creates a localization middleware that negotiates against the given bundle's String Catalog languages. /// - Parameter bundle: the bundle whose String Catalog names the supported languages. public init( bundle: Bundle ) { self.negotiate = .init(bundle: bundle) } } // MARK: - RouterMiddleware extension LocalizationMiddleware: RouterMiddleware { // MARK: Functions /// Negotiates the request's language and records it on the context before passing it down. /// - Parameters: /// - request: the incoming request. /// - context: the context the request is resolved against. /// - next: the next responder in the middleware chain. /// - Returns: the downstream response. /// - Throws: any error thrown downstream. public func handle( _ request: Request, context: Context, next: (Request, Context) async throws -> Response ) 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] ) return try await next(request, context) } } // 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" } }