import Foundation import Localization /// Negotiates the best supported language for a request from its `Accept-Language` header. /// /// Bound to the module's catalog languages via ``LanguageList``, an instance is invoked like a /// function — through ``callAsFunction(forAcceptLanguage:)`` — to resolve a header value to a /// supported language identifier, falling back to the default language. struct NegotiateLanguage { // MARK: Properties /// The supported languages and default language, derived from the module's String Catalog. private let list: LanguageList // MARK: Initializers /// Creates a language negotiator backed by the module's String Catalog. init() { self.list = .init(bundle: .module) } // MARK: Methods /// Picks the best supported language for the given `Accept-Language` header value. /// /// Invoked by calling the instance directly, for example `negotiate(forAcceptLanguage: header)`. /// The header is split into its language tags (dropping any `q` weights), then matched against /// the supported languages with `Bundle.preferredLocalizations(from:forPreferences:)`. When the /// header is absent or matches nothing, the default language is returned. /// - Parameter acceptLanguage: the raw `Accept-Language` header value, if any. /// - Returns: the identifier of the supported language to serve. func callAsFunction( forAcceptLanguage acceptLanguage: String? ) -> String { guard let acceptLanguage else { return list.default } let tags = tags(from: acceptLanguage) guard !tags.isEmpty else { return list.default } let preferred = Bundle.preferredLocalizations( from: list.all, forPreferences: tags ) return preferred.first ?? list.default } } // MARK: - Helpers private extension NegotiateLanguage { // MARK: Methods /// Extracts the ordered language tags from an `Accept-Language` header value. /// /// Each comma-separated entry is reduced to its language tag by dropping the `;q=` weight, and /// blank entries are removed. The original order is kept, which mirrors descending preference /// closely enough for `preferredLocalizations(from:forPreferences:)` to resolve correctly. /// - Parameter acceptLanguage: the raw `Accept-Language` header value. /// - Returns: the ordered, weight-stripped language tags. func tags( from acceptLanguage: String ) -> [String] { acceptLanguage .split(separator: ",") .map { entry in entry .split(separator: ";") .first .map(String.init)? .trimmingCharacters(in: .whitespaces) ?? "" } .filter { !$0.isEmpty } } }