commit 43121e80950668f9f942dd90483e749a2838053b
parent d9aded4d7059b7457a734b135d2717012592d579
Author: triesap <tyson@radroots.org>
Date: Fri, 2 Oct 2026 13:20:49 +0000
ios: localize typed values without wire drift
- Preserve every admitted decimal digit during native presentation
- Share localized price and quantity across visible and accessible cards
- Check authored dates before native conversion and display
- Qualify locale precision canonical continuity and accessible rendering
Diffstat:
10 files changed, 197 insertions(+), 12 deletions(-)
diff --git a/Package.swift b/Package.swift
@@ -112,6 +112,8 @@ let package = Package(
"Runtime/TeraCalendarEditing.swift",
"Runtime/TeraCivilDateInput.swift",
"Runtime/TeraFoodDecimalEntry.swift",
+ "Runtime/TeraExactDecimalPresentation.swift",
+ "Runtime/TeraFoodPresentation.swift",
"Runtime/TeraCivilDateFormat.swift",
"Runtime/TeraCheckedTime.swift",
"Runtime/TeraComposerModels.swift",
diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj
@@ -51,6 +51,7 @@
4DDC1E0EC1194DD15B2B19A3 /* TeraSubmissionReservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4721B7A69E2FB989732A64D /* TeraSubmissionReservationTests.swift */; };
53651EBAF42E8698717CE936 /* TeraScopedStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF28A9D711806A07A4A65EA8 /* TeraScopedStoreTests.swift */; };
542968135071D02F02BD4614 /* TeraAccessibilityUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D159B8D5E2417578E1C88FC1 /* TeraAccessibilityUITests.swift */; };
+ 5813921C9074F00625F87E06 /* TeraExactDecimalPresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F617B18A76B833F3575487F /* TeraExactDecimalPresentationTests.swift */; };
5CA871667D410B9553BF2BF6 /* TeraCapacitySettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF84D1BEAAAF218A58EE58D6 /* TeraCapacitySettingsTests.swift */; };
5DFD8053E97DED72B42BF528 /* TeraRecoveryInventoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F0FDC0D59BFCB5D7F928D5 /* TeraRecoveryInventoryTests.swift */; };
5F252ABA8CA48D5E3A7A0157 /* TeraMediaVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E1E87F9A11E85700F38635 /* TeraMediaVisibilityTests.swift */; };
@@ -228,6 +229,7 @@
6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellTests.swift; sourceTree = "<group>"; };
6D2BE93A0818202625CAF1F5 /* TeraTodayCalendarScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayCalendarScopeTests.swift; sourceTree = "<group>"; };
6D536D6625083ACB3DA4A674 /* TeraComposerDurabilityBoundaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraComposerDurabilityBoundaryTests.swift; sourceTree = "<group>"; };
+ 6F617B18A76B833F3575487F /* TeraExactDecimalPresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraExactDecimalPresentationTests.swift; sourceTree = "<group>"; };
70A358431863E54EDDAA24F4 /* TeraLocalBackupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraLocalBackupTests.swift; sourceTree = "<group>"; };
714B4DD70B881D29A89812C9 /* TeraTodayReconciliationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationTests.swift; sourceTree = "<group>"; };
7283D1C8F6E90A236BC84AF6 /* TeraTodayRenderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayRenderingTests.swift; sourceTree = "<group>"; };
@@ -411,6 +413,7 @@
B310FD8CCA9F9C2DA19BE8CF /* TeraEditingReplacementFFITests.swift */,
0F97ECE43BB7028B5A497720 /* TeraEditingReplacementTests.swift */,
93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */,
+ 6F617B18A76B833F3575487F /* TeraExactDecimalPresentationTests.swift */,
547AD45DC8C58FC4AA0D073D /* TeraFoodDecimalEntryTests.swift */,
99681A076A08E385942344F7 /* TeraForegroundSubmissionTests.swift */,
F666094C5E385A2820F0FE82 /* TeraIdentityCancellationTests.swift */,
@@ -743,6 +746,7 @@
888B178679E012BB053FE31B /* TeraEditingReplacementFFITests.swift in Sources */,
7B8C7C130DC15AF0266F2544 /* TeraEditingReplacementTests.swift in Sources */,
D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */,
+ 5813921C9074F00625F87E06 /* TeraExactDecimalPresentationTests.swift in Sources */,
116EF45DA06343BA3FA697AD /* TeraFoodDecimalEntryTests.swift in Sources */,
9A7E808CB591126300D3A118 /* TeraForegroundSubmissionTests.swift in Sources */,
41BD76CAA07F6071411F7AC1 /* TeraIdentityCancellationTests.swift in Sources */,
diff --git a/Tera/Runtime/TeraCalendarPresentation.swift b/Tera/Runtime/TeraCalendarPresentation.swift
@@ -29,16 +29,30 @@ struct TeraCalendarPresentation {
}
}
+ func authoredSummary(_ seconds: UInt64) -> String {
+ instant(seconds, zone: timeZone, label: "Posted")
+ }
+
+ func authoredDate(_ seconds: UInt64) -> Date? {
+ displayDate(seconds, zone: timeZone)
+ }
+
+ private func displayDate(_ seconds: UInt64, zone: TimeZone) -> Date? {
+ guard let date = TeraCalendarTiming.presentationInstant(seconds) else { return nil }
+ var calendar = Calendar(identifier: .gregorian)
+ calendar.timeZone = zone
+ return (1 ... 9999).contains(calendar.component(.year, from: date)) ? date : nil
+ }
+
private func sourceInstant(_ seconds: UInt64, zoneID: String, label: String) -> String {
guard zoneID.utf8.count <= 255, let zone = TimeZone(identifier: zoneID) else { return "Date unavailable" }
return "\(instant(seconds, zone: zone, label: label)) (\(zoneID))"
}
private func instant(_ seconds: UInt64, zone: TimeZone, label: String) -> String {
- guard let date = TeraCalendarTiming.presentationInstant(seconds) else { return "Date unavailable" }
+ guard let date = displayDate(seconds, zone: zone) else { return "Date unavailable" }
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = zone
- guard (1 ... 9999).contains(calendar.component(.year, from: date)) else { return "Date unavailable" }
let format = DateFormatter()
format.locale = locale
format.calendar = calendar
@@ -65,8 +79,12 @@ extension TeraTodayCard {
if let calendarTiming {
parts.append(TeraCalendarPresentation(locale: locale, timeZone: timeZone).summary(calendarTiming))
}
- if let priceSummary {
- parts.append(priceSummary)
+ let food = TeraFoodPresentation(locale: locale)
+ if let price = food.price(self) {
+ parts.append(price)
+ }
+ if let quantity = food.quantity(self) {
+ parts.append(quantity)
}
if lifecycle != .active {
parts.append(lifecycle.rawValue)
diff --git a/Tera/Runtime/TeraExactDecimalPresentation.swift b/Tera/Runtime/TeraExactDecimalPresentation.swift
@@ -0,0 +1,46 @@
+import Foundation
+
+/// Display admitted wire decimals as strings. NumberFormatter supplies locale
+/// symbols and grouping only; it never receives the full monetary value.
+struct TeraExactDecimalPresentation {
+ var locale: Locale = .current
+
+ func string(_ canonical: String) -> String? {
+ // The public food contract admits at most 28 digits plus one decimal dot.
+ // This is a bounded presentation guard, not a second publication validator.
+ guard canonical.utf8.count <= 29 else { return nil }
+ let parts = canonical.split(separator: ".", omittingEmptySubsequences: false)
+ guard (1 ... 2).contains(parts.count), parts.allSatisfy({ !$0.isEmpty }),
+ parts.allSatisfy({ $0.utf8.allSatisfy { (48 ... 57).contains($0) } }),
+ parts.reduce(0, { $0 + $1.utf8.count }) <= 28 else { return nil }
+ let format = NumberFormatter()
+ format.locale = locale
+ format.numberStyle = .decimal
+ let glyphs = (0 ... 9).map { format.string(from: NSNumber(value: $0)) ?? String($0) }
+ let whole = grouped(parts[0], format: format)
+ let localized = whole.utf8.map { byte -> String in
+ (48 ... 57).contains(byte) ? glyphs[Int(byte - 48)] : String(UnicodeScalar(byte))
+ }.joined()
+ // Translate digits before introducing possibly multibyte grouping symbols.
+ let integer = localized.replacingOccurrences(of: "|", with: format.groupingSeparator ?? "")
+ guard parts.count == 2 else { return integer }
+ let fraction = parts[1].utf8.map { glyphs[Int($0 - 48)] }.joined()
+ return integer + (format.decimalSeparator ?? ".") + fraction
+ }
+
+ private func grouped(_ whole: Substring, format: NumberFormatter) -> String {
+ let primary = format.groupingSize
+ guard format.usesGroupingSeparator, primary > 0, whole.count > primary else { return String(whole) }
+ let secondary = format.secondaryGroupingSize > 0 ? format.secondaryGroupingSize : primary
+ var end = whole.endIndex
+ var width = primary
+ var groups: [Substring] = []
+ while end > whole.startIndex {
+ let start = whole.index(end, offsetBy: -width, limitedBy: whole.startIndex) ?? whole.startIndex
+ groups.append(whole[start ..< end])
+ end = start
+ width = secondary
+ }
+ return groups.reversed().joined(separator: "|")
+ }
+}
diff --git a/Tera/Runtime/TeraFoodPresentation.swift b/Tera/Runtime/TeraFoodPresentation.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+struct TeraFoodPresentation {
+ var locale: Locale = .current
+
+ func price(_ card: TeraTodayCard) -> String? {
+ guard let amount = card.priceAmount, let currency = card.priceCurrency, let unit = card.priceUnit else { return nil }
+ guard let display = TeraExactDecimalPresentation(locale: locale).string(amount) else { return "Amount unavailable" }
+ return "\(display) \(currency)/\(unit)"
+ }
+
+ func quantity(_ card: TeraTodayCard) -> String? {
+ guard let quantity = card.quantity, let unit = card.priceUnit else { return nil }
+ guard let display = TeraExactDecimalPresentation(locale: locale).string(quantity) else { return "Quantity unavailable" }
+ return "\(display) \(unit) available"
+ }
+}
diff --git a/Tera/Views/TeraTodayCardPresentation.swift b/Tera/Views/TeraTodayCardPresentation.swift
@@ -34,9 +34,13 @@ enum TeraTodayCardPresentation {
if let timing = card.calendarTiming {
parts.append(TeraCalendarPresentation(locale: locale, timeZone: timeZone).summary(timing))
}
- if let price = card.priceSummary {
+ let food = TeraFoodPresentation(locale: locale)
+ if let price = food.price(card) {
parts.append(label(price))
}
+ if let quantity = food.quantity(card) {
+ parts.append(label(quantity))
+ }
if card.lifecycle != .active {
parts.append(card.lifecycle.rawValue)
}
diff --git a/Tera/Views/TeraTodayView.swift b/Tera/Views/TeraTodayView.swift
@@ -233,9 +233,11 @@ struct TeraTodayCardView: View {
}
HStack(spacing: 12) {
- Text(
- Date(timeIntervalSince1970: TimeInterval(card.authoredAtUnixSeconds)), style: .relative
- )
+ if let date = TeraCalendarPresentation(locale: locale, timeZone: timeZone).authoredDate(card.authoredAtUnixSeconds) {
+ Text(date, style: .relative)
+ } else {
+ Text("Date unavailable")
+ }
if card.lifecycle != .active {
Label(card.lifecycle.rawValue.capitalized, systemImage: "clock")
}
@@ -268,11 +270,12 @@ struct TeraTodayCardView: View {
@ViewBuilder
private var foodMetadataLabels: some View {
- if let price = card.priceSummary {
+ let food = TeraFoodPresentation(locale: locale)
+ if let price = food.price(card) {
Label(presentation.label(price), systemImage: "tag")
}
- if let quantity = card.quantity, let unit = card.priceUnit {
- Label(presentation.label("\(quantity) \(unit) available"), systemImage: "basket")
+ if let quantity = food.quantity(card) {
+ Label(presentation.label(quantity), systemImage: "basket")
}
if let location = card.location {
Label(presentation.label(location), systemImage: "mappin.and.ellipse")
diff --git a/TeraTests/TeraExactDecimalPresentationTests.swift b/TeraTests/TeraExactDecimalPresentationTests.swift
@@ -0,0 +1,90 @@
+import Foundation
+import SwiftUI
+@testable import TeraApp
+import XCTest
+
+final class TeraExactDecimalPresentationTests: XCTestCase {
+ func testAllAdmittedDigitsSurviveLocaleGroupingWithoutRounding() {
+ let raw = "12345678901234567890.12345678"
+ for (locale, expected) in [
+ ("en_US", "12,345,678,901,234,567,890.12345678"),
+ ("de_DE", "12.345.678.901.234.567.890,12345678"),
+ ("fr_FR", "12\u{202F}345\u{202F}678\u{202F}901\u{202F}234\u{202F}567\u{202F}890,12345678"),
+ ("ar_EG", "١٢٬٣٤٥٬٦٧٨٬٩٠١٬٢٣٤٬٥٦٧٬٨٩٠٫١٢٣٤٥٦٧٨"),
+ ("en_IN", "1,23,45,67,89,01,23,45,67,890.12345678"),
+ ] {
+ XCTAssertEqual(TeraExactDecimalPresentation(locale: Locale(identifier: locale)).string(raw), expected, locale)
+ }
+ XCTAssertEqual(raw, "12345678901234567890.12345678")
+ }
+
+ func testTinyFractionsAndLargestIntegersRetainEveryDigit() {
+ let format = TeraExactDecimalPresentation(locale: Locale(identifier: "de_DE"))
+ XCTAssertEqual(format.string("0.000000000000000000000000001"), "0,000000000000000000000000001")
+ XCTAssertEqual(format.string("9999999999999999999999999999"), "9.999.999.999.999.999.999.999.999.999")
+ XCTAssertEqual(format.string("0"), "0")
+ for raw in ["", "1.", ".1", "1e3", "1,5", "-1", "١", "1.2.3", String(repeating: "9", count: 29)] {
+ XCTAssertNil(format.string(raw), raw)
+ }
+ }
+
+ func testVisibleAndAccessibleFoodValuesUseTheSameExactLocalePresentation() {
+ let card = foodCard()
+ let original = card
+ let locale = Locale(identifier: "de_DE")
+ let format = TeraFoodPresentation(locale: locale)
+ XCTAssertEqual(format.price(card), "12.345,67890123456789 ZZZ/bag")
+ XCTAssertEqual(format.quantity(card), "0,000000000000000000000000001 bag available")
+ for mode: TeraTodayCardPresentation in [.feed, .detail] {
+ let summary = mode.accessibility(card, locale: locale, timeZone: .gmt)
+ XCTAssertTrue(summary.contains("12.345,67890123456789 ZZZ/bag"))
+ XCTAssertTrue(summary.contains("0,000000000000000000000000001 bag available"))
+ }
+ XCTAssertEqual(card, original)
+ XCTAssertEqual(card.priceAmount, "12345.67890123456789")
+ XCTAssertEqual(card.priceCurrency, "ZZZ")
+ XCTAssertEqual(card.priceUnit, "bag")
+ }
+
+ func testAuthoredInstantChecksTheNativeDomainAndLocalYearBeforeConversion() throws {
+ let format = TeraCalendarPresentation(locale: Locale(identifier: "en_US"), timeZone: .gmt)
+ XCTAssertTrue(format.authoredSummary(0).hasPrefix("Posted: Jan 1, 1970"))
+ XCTAssertEqual(format.authoredDate(0), Date(timeIntervalSince1970: 0))
+ for seconds: UInt64 in [253_402_300_800, (1 << 53) + 1, .max] {
+ XCTAssertEqual(format.authoredSummary(seconds), "Date unavailable")
+ }
+ let east = try XCTUnwrap(TimeZone(identifier: "Pacific/Kiritimati"))
+ XCTAssertEqual(TeraCalendarPresentation(timeZone: east).authoredSummary(253_402_300_799), "Date unavailable")
+ XCTAssertNil(TeraCalendarPresentation(timeZone: east).authoredDate(253_402_300_799))
+ }
+
+ @MainActor
+ func testProductionFoodCardRendersLocalizedValuesAtAccessibleSize() async throws {
+ let client = try await TeraScopeFixtures.client(TeraScopeBackend())
+ let store = TeraMediaStore(runtimeClient: client)
+ let view = TeraTodayCardView(card: foodCard(), context: nil, mediaStore: store, presentation: .detail)
+ .environment(\.locale, Locale(identifier: "de_DE"))
+ .environment(\.dynamicTypeSize, .accessibility5).padding(16).frame(width: 390)
+ let renderer = ImageRenderer(content: view)
+ let image = try XCTUnwrap(renderer.uiImage)
+ XCTAssertGreaterThan(image.size.height, 100)
+ let attachment = XCTAttachment(image: image)
+ attachment.name = "c122-exact-food-de-DE-accessibility5"
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ _ = try await client.stop()
+ }
+
+ private func foodCard() -> TeraTodayCard {
+ TeraTodayCard(
+ id: "food", type: .foodAvailability, sourceEventID: "event", sourceAddress: nil,
+ authorPublicKey: String(repeating: "a", count: 64), contractID: "test.food",
+ title: nil, content: "Seasonal food", authoredAtUnixSeconds: 0, effectiveAtUnixSeconds: 0,
+ calendarTiming: nil, location: nil, priceAmount: "12345.67890123456789", priceCurrency: "ZZZ",
+ priceUnit: "bag", quantity: "0.000000000000000000000000001", foodSummary: nil,
+ foodPublishedAtUnixSeconds: nil, foodStatus: nil, contextRank: 1, inclusionReason: "local",
+ media: [], lifecycle: .active, rankDigest: nil, authorProfile: nil, thread: [],
+ localOperationID: nil, localOperationState: nil
+ )
+ }
+}
diff --git a/project.yml b/project.yml
@@ -75,6 +75,7 @@ targets:
- path: TeraTests/TeraCoordinateAdmissionTests.swift
- path: TeraTests/TeraRevisionPreparationTests.swift
- path: TeraTests/TeraFoodDecimalEntryTests.swift
+ - path: TeraTests/TeraExactDecimalPresentationTests.swift
- path: TeraTests/TeraRevisionDetailTests.swift
- path: TeraTests/TeraRevisionTestBackend.swift
- path: TeraTests/TeraStoppedUploadTests.swift
diff --git a/release/provenance.json b/release/provenance.json
@@ -6,7 +6,7 @@
"info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef",
"privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12",
"sbom_sha256": "9e243176e3eaec256f404207d284314e8c96d3c6861077acb8e760640047ee2f",
- "xcode_project_sha256": "e95526f119f2e373f894ea32e63c353661bfceeae96d25d0bd5b797ca9caba09"
+ "xcode_project_sha256": "e5346577077bbb48a52caf97c7088d2cbebddca5a76b70855afd60c7aa99cc68"
},
"disposition": "unsigned",
"platforms": [