Project updates from Template (#1)

This PR contains the latest updates from the generic Website template, which have been added while working on #loud-amsterdam.

Reviewed-on: #1
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-09-04 13:40:35 +00:00
committed by javier
parent 08b4d80064
commit 65b62681eb
60 changed files with 2347 additions and 326 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
# Localization
The server-side localization toolkit the **Loud** services build on: locale-explicit String Catalog lookups, `Accept-Language` negotiation, and the catalog-derived language list — with no dependencies beyond Foundation.
The server-side localization toolkit the platform's services build on: locale-explicit String Catalog lookups, `Accept-Language` negotiation, and the catalog-derived language list — with no dependencies beyond Foundation.
## Overview
| Role | Types |
| --- | --- |
| Lookup | `Localize`, a bundle-bound localizer that resolves a catalog key for an explicit locale |
| Negotiation | `Negotiate`, which picks the best supported language from an `Accept-Language` header per RFC 9110 |
| Negotiation | `Negotiate`, which picks the best supported language from an explicit request or an `Accept-Language` header per RFC 9110 |
| Languages | `LanguageList`, the supported and default languages a bundle's String Catalog defines |
| Diagnostics | `CatalogState`, the outcome of reading the catalog (`loaded`, `missing`, or `undecodable`) |
@@ -24,18 +24,32 @@ public struct Negotiate: Sendable {
// MARK: Methods
/// Picks the best supported language for the given `Accept-Language` header value.
/// Picks the best supported language for the given explicit request and `Accept-Language` header value.
///
/// Invoked by calling the instance directly, for example `negotiate(acceptLanguage: header)`.
/// A `requested` language a deliberate choice, such as a language switcher's query parameter wins over the header when it matches a
/// supported language, on the same terms a header tag matches; an unsupported value is ignored, so it cannot select a language the catalog
/// does not serve.
///
/// The header is parsed into its language ranges, which are ordered by descending `q` weight as RFC 9110 prescribes: an entry without a weight
/// counts as 1, entries weighted 0 are "not acceptable" and dropped, and equal weights keep the header order. Each tag is then matched against the
/// supported languages in turn first by an exact match, then by its primary language subtag, so `de-AT` resolves to a supported `de` while the
/// `*` wildcard accepts the default language. When the header is absent or matches nothing, the default language is returned.
/// - Parameter acceptLanguage: the raw `Accept-Language` header value, if any.
/// - Parameters:
/// - requested: an explicitly requested language identifier, if any; it overrides the header when supported.
/// - acceptLanguage: the raw `Accept-Language` header value, if any.
/// - Returns: the identifier of the supported language to serve.
public func callAsFunction(
requested: String? = nil,
acceptLanguage language: String?
) -> String {
if
let requested,
let match = match(requested, in: list.all)
{
return match
}
guard let language else {
return list.default
}
@@ -127,4 +127,44 @@ struct NegotiateTests {
#expect(language == "en")
}
@Test
func `honours a supported requested language over the header`() {
let language = negotiate(
requested: "de",
acceptLanguage: "en"
)
#expect(language == "de")
}
@Test
func `matches a regional requested language by its primary subtag`() {
let language = negotiate(
requested: "de-AT",
acceptLanguage: "en"
)
#expect(language == "de")
}
@Test
func `ignores an unsupported requested language`() {
let language = negotiate(
requested: "fr",
acceptLanguage: "de"
)
#expect(language == "de")
}
@Test
func `ignores an empty requested language`() {
let language = negotiate(
requested: "",
acceptLanguage: "de"
)
#expect(language == "de")
}
}