# 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. ## Overview The package provides, grouped by role: | 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 | | 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`) | ## Design rules - **The String Catalog is the single source of truth.** Supported languages, the default language, and every string come from the bundle's `Localizable.xcstrings`. Adding a language is a translation-only change — once a locale exists in the catalog, `LanguageList` and `Negotiate` pick it up with no code change. - **The locale is always explicit.** A server has no single "current" locale, so every lookup names the locale to resolve in; nothing reads process-wide locale state. - **Raw `.xcstrings` parsing, for Linux parity.** The catalog is decoded from its JSON rather than through Foundation's compiled-catalog APIs, which are unavailable or non-functional on Linux. Consumers must `.copy` the catalog resource verbatim (not `.process` it) so it ships as raw JSON on every platform. Only simple `stringUnit` values are decoded; plural and device variations are not represented. - **Resolution never fails.** A missing entry falls back to the source-language string, then to the key itself; a missing or undecodable catalog degrades the language list to the default. Check `catalogState` once at startup and warn when it is not `.loaded`, before visitors ever see raw keys. - **Method structs.** `Localize` and `Negotiate` hold their lifetime-fixed configuration (the bundle) in `init` and take only per-call inputs in `callAsFunction`. - **One decoded catalog per bundle.** Catalogs are immutable at runtime, so every `Localize`, `Negotiate`, and `LanguageList` bound to the same bundle and table shares one cached `StringCatalog`. ## Layout Sources are split by visibility, then by kind, one type per file: ``` Sources/ ├── Public/ │ ├── Enumerations/ CatalogState │ ├── Methods/ Localize, Negotiate │ └── Types/ LanguageList └── Internal/ ├── Protocols/ CatalogResolving, the seam between the public API and the catalog backend └── Types/ StringCatalog (the cached .xcstrings decoder), LanguageRange Tests/ ├── Cases/ the test suites, mirroring the Sources/ layout ├── Catalogs/ the String Catalog fixture, copied verbatim so it loads on Linux └── Utils/ the StubCatalog resolver and the suite Tag constants ``` ## Testing Every suite carries a tag naming the kind of API it exercises — `.method` or `.type`, declared in `Tests/Utils/Extensions/Tag+Constants.swift` — so test plans and result summaries can slice the run by kind. A new suite must adopt the tag matching its subject (or add a tag there if none fits). ## Requirements - Swift 6.3 toolchain (`swift-tools-version:6.3`). - macOS 15, matching the sibling `Infrastructure` and `Persistence` packages (the services deploy to Linux containers; the packages carry no UI platforms). - No package dependencies — Foundation and Synchronization only.