From 814618c30329e64b43ee4a98576e06ef1eed67c2 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Wed, 4 Nov 2026 10:18:57 +0100 Subject: [PATCH] AI: give the formatter the currency the locale has not got MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a locale that carries no region -- `de` rather than `de_DE` -- NumberFormatter has no currency to format with, so Foundation renders ISO 4217's XXX, its code for "no currency", and every amount in the app reads "5,00 XXX" while the navigation title above it correctly says "Fr. abheben". Nothing downstream can repair that. currencyString() rewrites whichever markers the formatter reports, and in that locale currencySymbol is the generic placeholder "¤", which does not occur in the formatted string at all, while currencyCode is nil. An earlier attempt to special-case the literal "XXX" there was measured and did nothing, for exactly this reason. The currency belongs to the amount rather than to the locale, and the factory already has it, so hand it to the formatter. currencySymbol and currencyCode then carry real markers and the existing replacement works as it does for a locale with a region. A locale that does supply a currency keeps it. The guard in replacingCurrencyMarkers is required by the above: once both markers read "CHF", rewriting the same marker twice inserted the replacement's leading space twice and produced "5,00 CHF". Verified by A/B on the simulator, same tape, 10 screens each: without, --locale de 5 screens contain XXX; "Kurzwahl 5,00 XXX" with, --locale de 0 screens contain XXX; "Kurzwahl 5,00 CHF" with, --locale de_DE 0; identical to de_DE before the patch Assisted by LLM --- .../Helper/CurrencySpecification.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/TalerCommon/Helper/CurrencySpecification.swift b/TalerCommon/Helper/CurrencySpecification.swift index 5ea2b6ac..30ad151d 100644 --- a/TalerCommon/Helper/CurrencySpecification.swift +++ b/TalerCommon/Helper/CurrencySpecification.swift @@ -197,7 +197,11 @@ public struct CurrencyInfo: Sendable { if let symbol = formatter.currencySymbol, !symbol.isEmpty { result = result.replacingOccurrences(of: symbol, with: replacement) } - if let code = formatter.currencyCode, !code.isEmpty { + // Only if it is a different marker: for a currency the locale knows nothing + // about, ICU reports the code as the symbol too, and rewriting the same marker + // a second time inserts the replacement's leading space twice ("5,00 CHF"). + if let code = formatter.currencyCode, !code.isEmpty, + code != formatter.currencySymbol { result = result.replacingOccurrences(of: code, with: replacement) } return result @@ -371,6 +375,20 @@ public class CurrencyFormatter: NumberFormatter, @unchecked Sendable { } formatter.name = specs.name formatter.currency = currency + // `NumberFormatter´ takes the currency it formats with from its locale, and a + // locale with no region -- `de´ rather than `de_DE´ -- has none, so Foundation + // falls back to ISO 4217's code for "no currency" and every amount reads + // "5,00 XXX". Nothing downstream can repair that: `currencyString´ rewrites + // whichever markers the formatter reports, and `currencySymbol´ is then the + // generic "¤", which does not occur in the formatted string at all. + // The currency belongs to the amount, not to the locale, and it is right here -- + // so hand it to the formatter. `currencySymbol´ and `currencyCode´ then carry + // real markers, which `currencyString´ replaces with the alternative unit + // symbol, name or ISO code, exactly as it does for a locale that has a region. + // A locale which does supply a currency keeps it. + if (formatter.currencyCode as String?)?.isEmpty ?? true, !currency.isEmpty { + formatter.currencyCode = currency + } // formatter.setCode(to: EUR_4217) // formatter.setSymbol(to: "€") // clamp: specs comes from the exchange and is untrusted; an out-of-range or negative -- 2.47.3