From da1947da090527849f0e20fa8b13ebe7c8d08261 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Thu, 9 Apr 2026 20:04:24 +0200 Subject: [PATCH] fix iOS build blockers and add project scaffold --- PLAN.md | 7 +- contracts/localization/en.json | 2 + contracts/localization/sv.json | 2 + docs/localization-catalog.md | 2 + mobile/ios-app/App/HermesErrorMapper.swift | 1 + mobile/ios-app/App/RootView.swift | 3 +- .../Core/Analytics/AnalyticsClient.swift | 1 + .../Core/Localization/LocalizationStore.swift | 12 +- .../ios-app/Core/Networking/APIClient.swift | 3 +- .../ios-app/Core/Networking/APIModels.swift | 8 - mobile/ios-app/Features/Feed/FeedView.swift | 2 +- .../Features/Session/SessionView.swift | 79 +++-- .../Features/Settings/SettingsView.swift | 40 ++- .../HermesApp.xcodeproj/project.pbxproj | 303 ++++++++++++++++++ .../xcshareddata/xcschemes/HermesApp.xcscheme | 32 ++ .../Localization/en.lproj/Localizable.strings | 6 + .../Localization/sv.lproj/Localizable.strings | 6 + .../LocalizationStoreTests.swift | 38 +++ 18 files changed, 492 insertions(+), 55 deletions(-) create mode 100644 mobile/ios-app/HermesApp.xcodeproj/project.pbxproj create mode 100644 mobile/ios-app/HermesApp.xcodeproj/xcshareddata/xcschemes/HermesApp.xcscheme create mode 100644 mobile/ios-app/Tests/HermesAppTests/LocalizationStoreTests.swift diff --git a/PLAN.md b/PLAN.md index 92e0d8f..fcbf5e7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -13,6 +13,7 @@ This is the canonical working plan and progress log for the project. Use this fi - Backend smoke coverage was added for `server_time` and audit logging. - iOS now follows the backend-backed flow, syncs clock from `/health`, flushes analytics, and uses localized language labels instead of hardcoded `EN` and `SV`. - iOS source was updated for the backend-backed session and round flow, including the real preview cue points and localized session strings. +- iOS settings now uses localized copy instead of a scaffold placeholder, and the screen is wired into the root scroll flow. - Android debug build passes with `./gradlew :app:assembleDebug`. - Backend tests pass with `cargo test`. - Backend localization bundles now have a contract test, and the localization catalog matches the shipped English and Swedish keys. @@ -20,11 +21,13 @@ This is the canonical working plan and progress log for the project. Use this fi - Backend bet coverage now includes a direct regression for late-lock rejection. - Backend bet coverage now includes invalid-session and invalid-market rejection regressions, plus settlement override coverage. - Backend analytics coverage now includes a representative key user flow batch. +- iOS project scaffolding now exists as `mobile/ios-app/HermesApp.xcodeproj` with a shared scheme and XCTest target. +- iOS sources and tests typecheck against the iPhone simulator SDK, and a standalone localization/error-mapping harness passes. ### Still Open - Continue through the remaining plan phases and finish any leftover localization and polish work. -- Add iOS build validation once an Xcode project is available in `mobile/ios-app`. +- Run Xcode build/test validation once the local Xcode license is accepted; `xcodebuild` is currently blocked by the license gate in this environment. - Keep expanding tests around session, odds, settlement, and analytics behavior. ## 1. Purpose @@ -1310,6 +1313,8 @@ Must support flags for: The AI agent must work in this order. +After each major completed change set, create a git commit and push it before starting the next major change. + ### Phase 1: Documents 1. Write root README diff --git a/contracts/localization/en.json b/contracts/localization/en.json index d2e9bbc..f5f7f97 100644 --- a/contracts/localization/en.json +++ b/contracts/localization/en.json @@ -30,7 +30,9 @@ "result.outcome": "Outcome", "result.next_round": "Next round", "settings.title": "Settings", + "settings.subtitle": "Language, haptics and analytics preferences.", "settings.language": "Language", + "settings.enabled": "Enabled", "settings.haptics": "Haptics", "settings.analytics": "Analytics", "errors.generic": "Please try again.", diff --git a/contracts/localization/sv.json b/contracts/localization/sv.json index ab4ea11..bd760a0 100644 --- a/contracts/localization/sv.json +++ b/contracts/localization/sv.json @@ -30,7 +30,9 @@ "result.outcome": "Utfall", "result.next_round": "Nästa runda", "settings.title": "Inställningar", + "settings.subtitle": "Språk-, haptik- och analysinställningar.", "settings.language": "Språk", + "settings.enabled": "Aktiverad", "settings.haptics": "Haptik", "settings.analytics": "Analys", "errors.generic": "Försök igen.", diff --git a/docs/localization-catalog.md b/docs/localization-catalog.md index 3eb25c4..d2228cc 100644 --- a/docs/localization-catalog.md +++ b/docs/localization-catalog.md @@ -43,7 +43,9 @@ | `result.outcome` | Result outcome label | | `result.next_round` | Next round CTA | | `settings.title` | Settings title | +| `settings.subtitle` | Settings subtitle | | `settings.language` | Language setting | +| `settings.enabled` | Enabled status | | `settings.haptics` | Haptics setting | | `settings.analytics` | Analytics setting | | `errors.generic` | Generic error copy | diff --git a/mobile/ios-app/App/HermesErrorMapper.swift b/mobile/ios-app/App/HermesErrorMapper.swift index 82d3084..c852007 100644 --- a/mobile/ios-app/App/HermesErrorMapper.swift +++ b/mobile/ios-app/App/HermesErrorMapper.swift @@ -1,5 +1,6 @@ import Foundation +@MainActor func hermesUserFacingErrorMessage(localization: LocalizationStore, localeCode: String, error: Error?) -> String? { guard let error else { return nil diff --git a/mobile/ios-app/App/RootView.swift b/mobile/ios-app/App/RootView.swift index 20340fd..f263962 100644 --- a/mobile/ios-app/App/RootView.swift +++ b/mobile/ios-app/App/RootView.swift @@ -13,9 +13,10 @@ struct RootView: View { VStack(alignment: .leading, spacing: 24) { header OnboardingView(onStartSession: { onStartSession(localization.localeCode) }) - FeedView(onWatchPreview: {}, onRetry: { onStartSession(localization.localeCode) }) + FeedView(onRetry: { onStartSession(localization.localeCode) }) RoundView(onRetry: { onStartSession(localization.localeCode) }) SessionView(onRetry: { onStartSession(localization.localeCode) }) + SettingsView() } .padding(.horizontal, HermesTheme.screenPadding) .padding(.vertical, 24) diff --git a/mobile/ios-app/Core/Analytics/AnalyticsClient.swift b/mobile/ios-app/Core/Analytics/AnalyticsClient.swift index 09308e0..37dd70a 100644 --- a/mobile/ios-app/Core/Analytics/AnalyticsClient.swift +++ b/mobile/ios-app/Core/Analytics/AnalyticsClient.swift @@ -1,6 +1,7 @@ import Combine import Foundation +@MainActor protocol AnalyticsTracking { func track(_ event: String, attributes: [String: String]) } diff --git a/mobile/ios-app/Core/Localization/LocalizationStore.swift b/mobile/ios-app/Core/Localization/LocalizationStore.swift index d486f87..5d585d1 100644 --- a/mobile/ios-app/Core/Localization/LocalizationStore.swift +++ b/mobile/ios-app/Core/Localization/LocalizationStore.swift @@ -10,8 +10,8 @@ final class LocalizationStore: ObservableObject { private static let supportedLocaleCodes = ["en", "sv"] private static let fallbackLocaleCode = "en" - init(bundle: Bundle = .main, localeCode: String = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en") { - self.bundle = bundle + init(bundle: Bundle? = nil, localeCode: String = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en") { + self.bundle = bundle ?? Self.defaultBundle self.localeCode = Self.normalize(localeCode) } @@ -64,4 +64,12 @@ final class LocalizationStore: ObservableObject { return localeCode } + + private static var defaultBundle: Bundle { + #if SWIFT_PACKAGE + .module + #else + .main + #endif + } } diff --git a/mobile/ios-app/Core/Networking/APIClient.swift b/mobile/ios-app/Core/Networking/APIClient.swift index b901648..cb69730 100644 --- a/mobile/ios-app/Core/Networking/APIClient.swift +++ b/mobile/ios-app/Core/Networking/APIClient.swift @@ -91,7 +91,8 @@ struct HermesAPIClient { } func submitAnalyticsBatch(_ payload: HermesAnalyticsBatchRequest) async throws { - try await perform(path: "api/v1/analytics/batch", method: "POST", body: payload) + let encodedBody = try encoder.encode(payload) + _ = try await perform(path: "api/v1/analytics/batch", method: "POST", body: encodedBody) } private func send(path: String, method: String = "GET") async throws -> Response { diff --git a/mobile/ios-app/Core/Networking/APIModels.swift b/mobile/ios-app/Core/Networking/APIModels.swift index a296e4c..b7bb6fb 100644 --- a/mobile/ios-app/Core/Networking/APIModels.swift +++ b/mobile/ios-app/Core/Networking/APIModels.swift @@ -113,14 +113,6 @@ struct HermesRound: Codable { var settlement: HermesSettlement } -struct HermesRound: Codable { - var event: HermesEvent - var media: HermesEventMedia - var market: HermesMarket - var oddsVersion: HermesOddsVersion - var settlement: HermesSettlement -} - struct HermesBetIntentRequest: Codable { var sessionId: UUID var eventId: UUID diff --git a/mobile/ios-app/Features/Feed/FeedView.swift b/mobile/ios-app/Features/Feed/FeedView.swift index ca3b857..afcd5a9 100644 --- a/mobile/ios-app/Features/Feed/FeedView.swift +++ b/mobile/ios-app/Features/Feed/FeedView.swift @@ -92,7 +92,7 @@ struct FeedView: View { .font(.title2.weight(.bold)) .foregroundStyle(HermesTheme.textPrimary) - Text(round.map { localization.string(for: "feed.hero_subtitle") } ?? localization.string(for: "feed.hero_subtitle")) + Text(localization.string(for: "feed.hero_subtitle")) .font(.callout) .foregroundStyle(HermesTheme.textSecondary) .frame(maxWidth: 260, alignment: .leading) diff --git a/mobile/ios-app/Features/Session/SessionView.swift b/mobile/ios-app/Features/Session/SessionView.swift index 0e2edb2..a58cd70 100644 --- a/mobile/ios-app/Features/Session/SessionView.swift +++ b/mobile/ios-app/Features/Session/SessionView.swift @@ -23,51 +23,50 @@ struct SessionView: View { statusText = localization.string(for: "session.status_loading") } - return HermesCard { - VStack(alignment: .leading, spacing: HermesTheme.sectionSpacing) { - HermesSectionHeader( - title: localization.string(for: "session.title"), - subtitle: localization.string(for: "session.subtitle") - ) + return VStack(alignment: .leading, spacing: HermesTheme.sectionSpacing) { + HermesSectionHeader( + title: localization.string(for: "session.title"), + subtitle: localization.string(for: "session.subtitle") + ) - if session == nil { - if let bannerMessage { - sessionErrorState( - message: bannerMessage, - retryText: localization.string(for: "common.retry"), - onRetry: onRetry - ) - } else { - sessionLoadingState( - title: statusText, - subtitle: localization.string(for: "session.note") - ) - } - } else { - sessionStatusBadge(text: statusText, warning: bannerMessage != nil) - - if let bannerMessage { - sessionBanner(message: bannerMessage) - } - - sessionRow(label: localization.string(for: "session.id_label"), value: session?.sessionId.uuidString ?? "--") - sessionRow(label: localization.string(for: "session.user_id_label"), value: session?.userId.uuidString ?? "--") - sessionRow( - label: localization.string(for: "session.locale_label"), - value: localization.localeName(for: session?.localeCode ?? localization.localeCode) + if session == nil { + if let bannerMessage { + sessionErrorState( + message: bannerMessage, + retryText: localization.string(for: "common.retry"), + onRetry: onRetry + ) + } else { + sessionLoadingState( + title: statusText, + subtitle: localization.string(for: "session.note") ) - sessionRow(label: localization.string(for: "session.started_label"), value: session.map { Self.compactDateFormatter.string(from: $0.startedAt) } ?? "--") - sessionRow(label: localization.string(for: "session.variant_label"), value: session?.experimentVariant ?? "--") - sessionRow(label: localization.string(for: "session.app_version_label"), value: session?.appVersion ?? "--") - sessionRow(label: localization.string(for: "session.device_model_label"), value: session?.deviceModel ?? "--") - sessionRow(label: localization.string(for: "session.os_version_label"), value: session?.osVersion ?? "--") - - Text(localization.string(for: "session.note")) - .font(.callout) - .foregroundStyle(HermesTheme.textSecondary) } + } else { + sessionStatusBadge(text: statusText, warning: bannerMessage != nil) + + if let bannerMessage { + sessionBanner(message: bannerMessage) + } + + sessionRow(label: localization.string(for: "session.id_label"), value: session?.sessionId.uuidString ?? "--") + sessionRow(label: localization.string(for: "session.user_id_label"), value: session?.userId.uuidString ?? "--") + sessionRow( + label: localization.string(for: "session.locale_label"), + value: localization.localeName(for: session?.localeCode ?? localization.localeCode) + ) + sessionRow(label: localization.string(for: "session.started_label"), value: session.map { Self.compactDateFormatter.string(from: $0.startedAt) } ?? "--") + sessionRow(label: localization.string(for: "session.variant_label"), value: session?.experimentVariant ?? "--") + sessionRow(label: localization.string(for: "session.app_version_label"), value: session?.appVersion ?? "--") + sessionRow(label: localization.string(for: "session.device_model_label"), value: session?.deviceModel ?? "--") + sessionRow(label: localization.string(for: "session.os_version_label"), value: session?.osVersion ?? "--") + + Text(localization.string(for: "session.note")) + .font(.callout) + .foregroundStyle(HermesTheme.textSecondary) } } + .hermesCard() .onAppear { analytics.track("screen_viewed", attributes: ["screen_name": "session"]) } diff --git a/mobile/ios-app/Features/Settings/SettingsView.swift b/mobile/ios-app/Features/Settings/SettingsView.swift index 04bdbde..d721faf 100644 --- a/mobile/ios-app/Features/Settings/SettingsView.swift +++ b/mobile/ios-app/Features/Settings/SettingsView.swift @@ -1,7 +1,45 @@ import SwiftUI struct SettingsView: View { + @EnvironmentObject private var localization: LocalizationStore + var body: some View { - Text("Settings scaffold") + VStack(alignment: .leading, spacing: HermesTheme.sectionSpacing) { + HermesSectionHeader( + title: localization.string(for: "settings.title"), + subtitle: localization.string(for: "settings.subtitle") + ) + + VStack(spacing: 12) { + settingRow( + label: localization.string(for: "settings.language"), + value: localization.localeName(for: localization.localeCode) + ) + + settingRow( + label: localization.string(for: "settings.haptics"), + value: localization.string(for: "settings.enabled") + ) + + settingRow( + label: localization.string(for: "settings.analytics"), + value: localization.string(for: "settings.enabled") + ) + } + } + .hermesCard() + } + + @ViewBuilder + private func settingRow(label: String, value: String) -> some View { + HStack { + Text(label) + .font(.callout) + .foregroundStyle(HermesTheme.textSecondary) + Spacer(minLength: 12) + Text(value) + .font(.callout.weight(.semibold)) + .foregroundStyle(HermesTheme.textPrimary) + } } } diff --git a/mobile/ios-app/HermesApp.xcodeproj/project.pbxproj b/mobile/ios-app/HermesApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a6e24d4 --- /dev/null +++ b/mobile/ios-app/HermesApp.xcodeproj/project.pbxproj @@ -0,0 +1,303 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 3E1C98162835584FF4929E35 = {isa = PBXBuildFile; fileRef = AFDCE723CFA31DF2A91DA374; }; + 706106BBB737DEF1952FED6F = {isa = PBXBuildFile; fileRef = 3686935FA33F0278440C2137; }; + 594A855A0BE13416458DA3A6 = {isa = PBXBuildFile; fileRef = DA1E3F27959B70E2FA168E86; }; + D3B7534C86065FFF51A5C55D = {isa = PBXBuildFile; fileRef = 6DE0108803DE783A7D5D528C; }; + 2D43EFE45D26F6A8E55BF3AC = {isa = PBXBuildFile; fileRef = 895DC0E4CCA3A72E5897D850; }; + F9006995CFE1CCF0D38AD3AA = {isa = PBXBuildFile; fileRef = A24F440D4AC95A5E5A294BB5; }; + 913CFC8BADBC80B024D2C856 = {isa = PBXBuildFile; fileRef = E727CD23C8DFCD3FFA972118; }; + AD46D096136FB1F118855D68 = {isa = PBXBuildFile; fileRef = 09BEEF1EACD1683731EA8B13; }; + B3C9966AC80EBB9B64A07C99 = {isa = PBXBuildFile; fileRef = 618EEBF90A2888756D2A0D3D; }; + BFC8A4A81EE80733E4AA7323 = {isa = PBXBuildFile; fileRef = 3C969AD72CFEAAFD87172BC5; }; + 2C7F98F2653480078868DBF4 = {isa = PBXBuildFile; fileRef = 06986680EEC52DA119F3C422; }; + 5384DC2D3DC830A67B2A1054 = {isa = PBXBuildFile; fileRef = 154F342749182EA88D7D3EA2; }; + 1E2027DC632793AF7E962016 = {isa = PBXBuildFile; fileRef = 2927929745A862DD7DB483BE; }; + C1DB325D4807CDE56F820173 = {isa = PBXBuildFile; fileRef = 738C6476A9F7A2BBCA018EC7; }; + 1A95F35F994CB9F20DF09556 = {isa = PBXBuildFile; fileRef = A6E7323ECDEAA449DD2925F0; }; + 9BA60A645924EEE65B60E9C1 = {isa = PBXBuildFile; fileRef = 29BBCB2FC555E6C905C380A7; }; + F2FA36C9D20CD77F0045E16E = {isa = PBXBuildFile; fileRef = D34D3C5C813EAB2AD265F9CE; }; + 72B9487B80FFA7D58F0CF66A = {isa = PBXBuildFile; fileRef = 16E0F69DFE5FC784FB2BBECA; }; + 739163E3DBD5F43A84C49818 = {isa = PBXBuildFile; fileRef = 78ADAFCECDB66900EB275E3D; }; + A99DDCF27A5B1368CCA1EFB5 = {isa = PBXBuildFile; fileRef = B9C59E00C07DD533A7DC8126; }; + C8B084764EFF305D4AD0ADDF = {isa = PBXBuildFile; fileRef = 55B391E8147E161F76672032; }; + FCF2BB724CCC6B6974C426F8 = {isa = PBXBuildFile; fileRef = 36A67B6FF0A52868D30E3AFF; }; + 7E17EFEA80A73B8C3179D66C = {isa = PBXBuildFile; fileRef = 5E777846BC9ED35ED93034AA; }; + 9FE2C1B4C0DA9FE3AEF298AF = {isa = PBXBuildFile; fileRef = 9710CA4A388CBE265276E667; }; +/* End PBXBuildFile section */ + + +/* Begin PBXContainerItemProxy section */ + CCF863E2FFE2061635563672 = {isa = PBXContainerItemProxy; containerPortal = 63E87276D8BD98B1C9DA3FF1; proxyType = 1; remoteGlobalIDString = E9FB55C6F47F1FFA66FE0D94; remoteInfo = HermesApp; }; +/* End PBXContainerItemProxy section */ + + +/* Begin PBXFileReference section */ + AFDCE723CFA31DF2A91DA374 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/HermesApp.swift; sourceTree = ""; }; + 3686935FA33F0278440C2137 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/HermesErrorMapper.swift; sourceTree = ""; }; + DA1E3F27959B70E2FA168E86 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/HermesRepository.swift; sourceTree = ""; }; + 6DE0108803DE783A7D5D528C = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RootView.swift; sourceTree = ""; }; + 895DC0E4CCA3A72E5897D850 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Analytics/AnalyticsClient.swift; sourceTree = ""; }; + A24F440D4AC95A5E5A294BB5 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/DesignSystem/Theme.swift; sourceTree = ""; }; + E727CD23C8DFCD3FFA972118 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Gestures/GestureHandlers.swift; sourceTree = ""; }; + 09BEEF1EACD1683731EA8B13 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Haptics/HapticsController.swift; sourceTree = ""; }; + 618EEBF90A2888756D2A0D3D = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Localization/LocalizationStore.swift; sourceTree = ""; }; + 3C969AD72CFEAAFD87172BC5 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Media/HermesVideoPlayerView.swift; sourceTree = ""; }; + 06986680EEC52DA119F3C422 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Media/PlayerCoordinator.swift; sourceTree = ""; }; + 154F342749182EA88D7D3EA2 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Networking/APIClient.swift; sourceTree = ""; }; + 2927929745A862DD7DB483BE = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Networking/APIModels.swift; sourceTree = ""; }; + 738C6476A9F7A2BBCA018EC7 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Feed/FeedView.swift; sourceTree = ""; }; + A6E7323ECDEAA449DD2925F0 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Onboarding/OnboardingView.swift; sourceTree = ""; }; + 29BBCB2FC555E6C905C380A7 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Result/ResultView.swift; sourceTree = ""; }; + D34D3C5C813EAB2AD265F9CE = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Reveal/RevealView.swift; sourceTree = ""; }; + 16E0F69DFE5FC784FB2BBECA = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Round/RoundView.swift; sourceTree = ""; }; + 78ADAFCECDB66900EB275E3D = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Selection/SelectionView.swift; sourceTree = ""; }; + B9C59E00C07DD533A7DC8126 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Session/SessionView.swift; sourceTree = ""; }; + 55B391E8147E161F76672032 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Settings/SettingsView.swift; sourceTree = ""; }; + 36A67B6FF0A52868D30E3AFF = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests/HermesAppTests/LocalizationStoreTests.swift; sourceTree = ""; }; + 5E777846BC9ED35ED93034AA = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Resources/Localization/en.lproj/Localizable.strings; sourceTree = ""; }; + 9710CA4A388CBE265276E667 = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Resources/Localization/sv.lproj/Localizable.strings; sourceTree = ""; }; + 3A8D22D1A79B415AD4892F84 = {isa = PBXFileReference; explicitFileType = wrapper.application; path = HermesApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C1368E6B7C6BAAA50C8B38A8 = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = HermesAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + + +/* Begin PBXGroup section */ + 5151B46607EBC8890D8DFCC2 = {isa = PBXGroup; children = ( + 3A8D22D1A79B415AD4892F84, + C1368E6B7C6BAAA50C8B38A8, +); sourceTree = ""; }; + 1C48CE29E4E135EE62951B22 = {isa = PBXGroup; children = ( + AFDCE723CFA31DF2A91DA374, + 3686935FA33F0278440C2137, + DA1E3F27959B70E2FA168E86, + 6DE0108803DE783A7D5D528C, +); path = App; sourceTree = ""; }; + 410188F9DC6E499FEC68BCF6 = {isa = PBXGroup; children = ( + 895DC0E4CCA3A72E5897D850, + A24F440D4AC95A5E5A294BB5, + E727CD23C8DFCD3FFA972118, + 09BEEF1EACD1683731EA8B13, + 618EEBF90A2888756D2A0D3D, + 3C969AD72CFEAAFD87172BC5, + 06986680EEC52DA119F3C422, + 154F342749182EA88D7D3EA2, + 2927929745A862DD7DB483BE, +); path = Core; sourceTree = ""; }; + 1F26A8704D8CDB03D5D3D4BB = {isa = PBXGroup; children = ( + 738C6476A9F7A2BBCA018EC7, + A6E7323ECDEAA449DD2925F0, + 29BBCB2FC555E6C905C380A7, + D34D3C5C813EAB2AD265F9CE, + 16E0F69DFE5FC784FB2BBECA, + 78ADAFCECDB66900EB275E3D, + B9C59E00C07DD533A7DC8126, + 55B391E8147E161F76672032, +); path = Features; sourceTree = ""; }; + A024F06580C79B7582C3806A = {isa = PBXGroup; children = ( + 5E777846BC9ED35ED93034AA, + 9710CA4A388CBE265276E667, +); path = Resources; sourceTree = ""; }; + 156052F04525D0F710673CF0 = {isa = PBXGroup; children = ( + 36A67B6FF0A52868D30E3AFF, +); path = Tests; sourceTree = ""; }; + E461FA3D1EC1D072C116F41B = {isa = PBXGroup; children = ( + 1C48CE29E4E135EE62951B22, + 410188F9DC6E499FEC68BCF6, + 1F26A8704D8CDB03D5D3D4BB, + A024F06580C79B7582C3806A, + 156052F04525D0F710673CF0, + 5151B46607EBC8890D8DFCC2, +); sourceTree = ""; }; +/* End PBXGroup section */ + + +/* Begin PBXNativeTarget section */ + E9FB55C6F47F1FFA66FE0D94 = {isa = PBXNativeTarget; buildConfigurationList = 6999223B144664B5601B7EE0; buildPhases = ( + B70F6135A878AF9C52651F99, + 42F0FBEA30B4CA87317E015D, + DFA634CB8CCD163EA3930D97, +); buildRules = (); dependencies = (); name = HermesApp; productName = HermesApp; productReference = 3A8D22D1A79B415AD4892F84; productType = "com.apple.product-type.application"; }; + 32127D1229D93E1AD0281CF5 = {isa = PBXNativeTarget; buildConfigurationList = 82A4B6F3DEB17576E838F4C6; buildPhases = ( + 0A58D7436E03725AA2604F91, + 4F6B784AA6FDB9607A3034BB, +); buildRules = (); dependencies = ( + 54AF1B5F109B8B15F06D32EB, +); name = HermesAppTests; productName = HermesAppTests; productReference = C1368E6B7C6BAAA50C8B38A8; productType = "com.apple.product-type.bundle.unit-test"; }; +/* End PBXNativeTarget section */ + + +/* Begin PBXProject section */ + 63E87276D8BD98B1C9DA3FF1 = {isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1600; }; buildConfigurationList = FCBE72429C0B1B8B87DC8CC9; compatibilityVersion = "Xcode 16.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( + en, + sv, +); mainGroup = E461FA3D1EC1D072C116F41B; packageReferences = (); productRefGroup = 5151B46607EBC8890D8DFCC2; projectDirPath = ""; projectRoot = ""; targets = ( + E9FB55C6F47F1FFA66FE0D94, + 32127D1229D93E1AD0281CF5, +); }; +/* End PBXProject section */ + + +/* Begin PBXTargetDependency section */ + 54AF1B5F109B8B15F06D32EB = {isa = PBXTargetDependency; target = E9FB55C6F47F1FFA66FE0D94; targetProxy = CCF863E2FFE2061635563672; }; +/* End PBXTargetDependency section */ + + +/* Begin PBXFrameworksBuildPhase section */ + B70F6135A878AF9C52651F99 = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; }; + 0A58D7436E03725AA2604F91 = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXFrameworksBuildPhase section */ + + +/* Begin PBXSourcesBuildPhase section */ + 42F0FBEA30B4CA87317E015D = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3E1C98162835584FF4929E35, + 706106BBB737DEF1952FED6F, + 594A855A0BE13416458DA3A6, + D3B7534C86065FFF51A5C55D, + 2D43EFE45D26F6A8E55BF3AC, + F9006995CFE1CCF0D38AD3AA, + 913CFC8BADBC80B024D2C856, + AD46D096136FB1F118855D68, + B3C9966AC80EBB9B64A07C99, + BFC8A4A81EE80733E4AA7323, + 2C7F98F2653480078868DBF4, + 5384DC2D3DC830A67B2A1054, + 1E2027DC632793AF7E962016, + C1DB325D4807CDE56F820173, + 1A95F35F994CB9F20DF09556, + 9BA60A645924EEE65B60E9C1, + F2FA36C9D20CD77F0045E16E, + 72B9487B80FFA7D58F0CF66A, + 739163E3DBD5F43A84C49818, + A99DDCF27A5B1368CCA1EFB5, + C8B084764EFF305D4AD0ADDF, +); runOnlyForDeploymentPostprocessing = 0; }; + 4F6B784AA6FDB9607A3034BB = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FCF2BB724CCC6B6974C426F8, +); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXSourcesBuildPhase section */ + + +/* Begin PBXResourcesBuildPhase section */ + DFA634CB8CCD163EA3930D97 = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7E17EFEA80A73B8C3179D66C, + 9FE2C1B4C0DA9FE3AEF298AF, +); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXResourcesBuildPhase section */ + + +/* Begin XCBuildConfiguration section */ + C761B7D49C3C32C5F60E8010 = {isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_REGION = en; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_VERSION = 5.0; +}; name = Debug; }; + ACCCD85F61979F8ECCA377BC = {isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_REGION = en; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_VERSION = 5.0; +}; name = Release; }; + A393E557BBA174C592175785 = {isa = XCBuildConfiguration; buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hermes.app; + PRODUCT_NAME = HermesApp; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + SWIFT_OPTIMIZATION_LEVEL = -Onone; +}; name = Debug; }; + 4848DE5FB3FF960B8E5B63DC = {isa = XCBuildConfiguration; buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hermes.app; + PRODUCT_NAME = HermesApp; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + SWIFT_OPTIMIZATION_LEVEL = -O; +}; name = Release; }; + 0D633BD545D878A6647A1EBE = {isa = XCBuildConfiguration; buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_TESTING_SEARCH_PATHS = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hermes.appTests; + PRODUCT_NAME = HermesAppTests; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HermesApp.app/HermesApp"; + SWIFT_OPTIMIZATION_LEVEL = -Onone; +}; name = Debug; }; + 43ABF9DDFB26DCC1D7D88137 = {isa = XCBuildConfiguration; buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_TESTING_SEARCH_PATHS = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.hermes.appTests; + PRODUCT_NAME = HermesAppTests; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HermesApp.app/HermesApp"; + SWIFT_OPTIMIZATION_LEVEL = -O; +}; name = Release; }; +/* End XCBuildConfiguration section */ + + +/* Begin XCConfigurationList section */ + FCBE72429C0B1B8B87DC8CC9 = {isa = XCConfigurationList; buildConfigurations = ( + C761B7D49C3C32C5F60E8010, + ACCCD85F61979F8ECCA377BC, +); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 6999223B144664B5601B7EE0 = {isa = XCConfigurationList; buildConfigurations = ( + A393E557BBA174C592175785, + 4848DE5FB3FF960B8E5B63DC, +); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 82A4B6F3DEB17576E838F4C6 = {isa = XCConfigurationList; buildConfigurations = ( + 0D633BD545D878A6647A1EBE, + 43ABF9DDFB26DCC1D7D88137, +); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; +/* End XCConfigurationList section */ + + }; + rootObject = 63E87276D8BD98B1C9DA3FF1; +} diff --git a/mobile/ios-app/HermesApp.xcodeproj/xcshareddata/xcschemes/HermesApp.xcscheme b/mobile/ios-app/HermesApp.xcodeproj/xcshareddata/xcschemes/HermesApp.xcscheme new file mode 100644 index 0000000..a79dd9c --- /dev/null +++ b/mobile/ios-app/HermesApp.xcodeproj/xcshareddata/xcschemes/HermesApp.xcscheme @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios-app/Resources/Localization/en.lproj/Localizable.strings b/mobile/ios-app/Resources/Localization/en.lproj/Localizable.strings index 52c7c1d..bb6e6ec 100644 --- a/mobile/ios-app/Resources/Localization/en.lproj/Localizable.strings +++ b/mobile/ios-app/Resources/Localization/en.lproj/Localizable.strings @@ -56,3 +56,9 @@ "session.app_version_label" = "App version"; "session.device_model_label" = "Device model"; "session.os_version_label" = "OS version"; +"settings.title" = "Settings"; +"settings.subtitle" = "Language, haptics and analytics preferences."; +"settings.language" = "Language"; +"settings.enabled" = "Enabled"; +"settings.haptics" = "Haptics"; +"settings.analytics" = "Analytics"; diff --git a/mobile/ios-app/Resources/Localization/sv.lproj/Localizable.strings b/mobile/ios-app/Resources/Localization/sv.lproj/Localizable.strings index 3426546..a0d258f 100644 --- a/mobile/ios-app/Resources/Localization/sv.lproj/Localizable.strings +++ b/mobile/ios-app/Resources/Localization/sv.lproj/Localizable.strings @@ -56,3 +56,9 @@ "session.app_version_label" = "Appversion"; "session.device_model_label" = "Enhetsmodell"; "session.os_version_label" = "OS-version"; +"settings.title" = "Inställningar"; +"settings.subtitle" = "Språk-, haptik- och analysinställningar."; +"settings.language" = "Språk"; +"settings.enabled" = "Aktiverad"; +"settings.haptics" = "Haptik"; +"settings.analytics" = "Analys"; diff --git a/mobile/ios-app/Tests/HermesAppTests/LocalizationStoreTests.swift b/mobile/ios-app/Tests/HermesAppTests/LocalizationStoreTests.swift new file mode 100644 index 0000000..8c129ae --- /dev/null +++ b/mobile/ios-app/Tests/HermesAppTests/LocalizationStoreTests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import HermesApp + +private struct TestFailure: Error, CustomStringConvertible { + let description: String +} + +@MainActor +final class LocalizationStoreTests: XCTestCase { + func testEnglishBundleStringsLoad() throws { + let store = LocalizationStore(localeCode: "en") + + try expectEqual(store.string(for: "app.name"), "Hermes") + try expectEqual(store.string(for: "settings.title"), "Settings") + try expectEqual(store.string(for: "settings.enabled"), "Enabled") + } + + func testSwedishBundleStringsLoad() throws { + let store = LocalizationStore(localeCode: "sv") + + try expectEqual(store.string(for: "app.name"), "Hermes") + try expectEqual(store.string(for: "settings.title"), "Inställningar") + try expectEqual(store.string(for: "settings.enabled"), "Aktiverad") + } + + func testUnsupportedLocaleFallsBackToEnglish() throws { + let store = LocalizationStore(localeCode: "fr") + + try expectEqual(store.string(for: "settings.analytics"), "Analytics") + try expectEqual(store.localeName(for: "sv", displayLocaleCode: "fr"), "Swedish") + } + + private func expectEqual(_ actual: String, _ expected: String) throws { + guard actual == expected else { + throw TestFailure(description: "Expected \(expected), got \(actual)") + } + } +}