import Elementary import Foundation import HTTPTypes import Hummingbird import Localization /// A per-language collection of pre-rendered HTML responses. /// /// At initialization it renders the document once for each language the bundle's ``LanguageList`` reports and caches the bytes, mirroring /// ``CachedHTMLResponse``'s render-once model but keyed by language. Each cached response carries a `Content-Language` header naming the /// language it was rendered in. /// /// `Vary: Accept-Language` is the caller's to declare, since only the caller knows how it picks the language: a responder that negotiates the /// header sets it, so shared caches key on the language rather than serving one to everyone; a responder that pins the language by route does /// not, since the header would announce a negotiation that never happens. public struct LocalizedHTMLCollectionResponse: Sendable { // MARK: Properties /// The supported languages and default language, derived from the bundle's String Catalog. private let list: LanguageList /// The pre-rendered responses, keyed by language identifier. private let responses: [String: CachedHTMLResponse] // MARK: Initializers /// Renders the document once per supported language. /// - Parameters: /// - bundle: the bundle whose String Catalog names the languages the document is rendered for. /// - status: the status applied to every response. Defaults to `.ok`. /// - variesOnAcceptLanguage: whether the responses declare `Vary: Accept-Language`. Defaults to `false`; a responder that /// negotiates the header passes `true`. /// - document: builds the document to render for a given locale. public init( bundle: Bundle, status: HTTPResponse.Status = .ok, variesOnAcceptLanguage: Bool = false, document: (Locale) -> Document ) { self.list = .init(bundle: bundle) self.responses = list.all .reduce(into: [:]) { responses, language in var headers: HTTPFields = [.contentLanguage: language] if variesOnAcceptLanguage { headers[.vary] = "Accept-Language" } responses[language] = CachedHTMLResponse( status: status, additionalHeaders: headers, document: document(.init( identifier: language )) ) } } // MARK: Methods /// Builds the response for the given language, falling back to the default language. /// - Parameters: /// - language: the negotiated language identifier. /// - request: the request the response answers, consulted for conditional revalidation. /// - Returns: the cached response for the language, the default language's response when the language is unavailable, or a /// `500 Internal Server Error` if neither is cached. public func response( for language: String, request: Request ) -> Response { guard let response = responses[language] ?? responses[list.default] else { return .init( status: .internalServerError ) } return response.response( for: request ) } }