fix iOS build blockers and add project scaffold

This commit is contained in:
2026-04-09 20:04:24 +02:00
parent 02278ddac7
commit da1947da09
18 changed files with 492 additions and 55 deletions
+6 -1
View File
@@ -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. - 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 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 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`. - Android debug build passes with `./gradlew :app:assembleDebug`.
- Backend tests pass with `cargo test`. - 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. - 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 a direct regression for late-lock rejection.
- Backend bet coverage now includes invalid-session and invalid-market rejection regressions, plus settlement override coverage. - 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. - 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 ### Still Open
- Continue through the remaining plan phases and finish any leftover localization and polish work. - 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. - Keep expanding tests around session, odds, settlement, and analytics behavior.
## 1. Purpose ## 1. Purpose
@@ -1310,6 +1313,8 @@ Must support flags for:
The AI agent must work in this order. 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 ### Phase 1: Documents
1. Write root README 1. Write root README
+2
View File
@@ -30,7 +30,9 @@
"result.outcome": "Outcome", "result.outcome": "Outcome",
"result.next_round": "Next round", "result.next_round": "Next round",
"settings.title": "Settings", "settings.title": "Settings",
"settings.subtitle": "Language, haptics and analytics preferences.",
"settings.language": "Language", "settings.language": "Language",
"settings.enabled": "Enabled",
"settings.haptics": "Haptics", "settings.haptics": "Haptics",
"settings.analytics": "Analytics", "settings.analytics": "Analytics",
"errors.generic": "Please try again.", "errors.generic": "Please try again.",
+2
View File
@@ -30,7 +30,9 @@
"result.outcome": "Utfall", "result.outcome": "Utfall",
"result.next_round": "Nästa runda", "result.next_round": "Nästa runda",
"settings.title": "Inställningar", "settings.title": "Inställningar",
"settings.subtitle": "Språk-, haptik- och analysinställningar.",
"settings.language": "Språk", "settings.language": "Språk",
"settings.enabled": "Aktiverad",
"settings.haptics": "Haptik", "settings.haptics": "Haptik",
"settings.analytics": "Analys", "settings.analytics": "Analys",
"errors.generic": "Försök igen.", "errors.generic": "Försök igen.",
+2
View File
@@ -43,7 +43,9 @@
| `result.outcome` | Result outcome label | | `result.outcome` | Result outcome label |
| `result.next_round` | Next round CTA | | `result.next_round` | Next round CTA |
| `settings.title` | Settings title | | `settings.title` | Settings title |
| `settings.subtitle` | Settings subtitle |
| `settings.language` | Language setting | | `settings.language` | Language setting |
| `settings.enabled` | Enabled status |
| `settings.haptics` | Haptics setting | | `settings.haptics` | Haptics setting |
| `settings.analytics` | Analytics setting | | `settings.analytics` | Analytics setting |
| `errors.generic` | Generic error copy | | `errors.generic` | Generic error copy |
@@ -1,5 +1,6 @@
import Foundation import Foundation
@MainActor
func hermesUserFacingErrorMessage(localization: LocalizationStore, localeCode: String, error: Error?) -> String? { func hermesUserFacingErrorMessage(localization: LocalizationStore, localeCode: String, error: Error?) -> String? {
guard let error else { guard let error else {
return nil return nil
+2 -1
View File
@@ -13,9 +13,10 @@ struct RootView: View {
VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 24) {
header header
OnboardingView(onStartSession: { onStartSession(localization.localeCode) }) OnboardingView(onStartSession: { onStartSession(localization.localeCode) })
FeedView(onWatchPreview: {}, onRetry: { onStartSession(localization.localeCode) }) FeedView(onRetry: { onStartSession(localization.localeCode) })
RoundView(onRetry: { onStartSession(localization.localeCode) }) RoundView(onRetry: { onStartSession(localization.localeCode) })
SessionView(onRetry: { onStartSession(localization.localeCode) }) SessionView(onRetry: { onStartSession(localization.localeCode) })
SettingsView()
} }
.padding(.horizontal, HermesTheme.screenPadding) .padding(.horizontal, HermesTheme.screenPadding)
.padding(.vertical, 24) .padding(.vertical, 24)
@@ -1,6 +1,7 @@
import Combine import Combine
import Foundation import Foundation
@MainActor
protocol AnalyticsTracking { protocol AnalyticsTracking {
func track(_ event: String, attributes: [String: String]) func track(_ event: String, attributes: [String: String])
} }
@@ -10,8 +10,8 @@ final class LocalizationStore: ObservableObject {
private static let supportedLocaleCodes = ["en", "sv"] private static let supportedLocaleCodes = ["en", "sv"]
private static let fallbackLocaleCode = "en" private static let fallbackLocaleCode = "en"
init(bundle: Bundle = .main, localeCode: String = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en") { init(bundle: Bundle? = nil, localeCode: String = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en") {
self.bundle = bundle self.bundle = bundle ?? Self.defaultBundle
self.localeCode = Self.normalize(localeCode) self.localeCode = Self.normalize(localeCode)
} }
@@ -64,4 +64,12 @@ final class LocalizationStore: ObservableObject {
return localeCode return localeCode
} }
private static var defaultBundle: Bundle {
#if SWIFT_PACKAGE
.module
#else
.main
#endif
}
} }
@@ -91,7 +91,8 @@ struct HermesAPIClient {
} }
func submitAnalyticsBatch(_ payload: HermesAnalyticsBatchRequest) async throws { 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<Response: Decodable>(path: String, method: String = "GET") async throws -> Response { private func send<Response: Decodable>(path: String, method: String = "GET") async throws -> Response {
@@ -113,14 +113,6 @@ struct HermesRound: Codable {
var settlement: HermesSettlement var settlement: HermesSettlement
} }
struct HermesRound: Codable {
var event: HermesEvent
var media: HermesEventMedia
var market: HermesMarket
var oddsVersion: HermesOddsVersion
var settlement: HermesSettlement
}
struct HermesBetIntentRequest: Codable { struct HermesBetIntentRequest: Codable {
var sessionId: UUID var sessionId: UUID
var eventId: UUID var eventId: UUID
+1 -1
View File
@@ -92,7 +92,7 @@ struct FeedView: View {
.font(.title2.weight(.bold)) .font(.title2.weight(.bold))
.foregroundStyle(HermesTheme.textPrimary) .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) .font(.callout)
.foregroundStyle(HermesTheme.textSecondary) .foregroundStyle(HermesTheme.textSecondary)
.frame(maxWidth: 260, alignment: .leading) .frame(maxWidth: 260, alignment: .leading)
@@ -23,8 +23,7 @@ struct SessionView: View {
statusText = localization.string(for: "session.status_loading") statusText = localization.string(for: "session.status_loading")
} }
return HermesCard { return VStack(alignment: .leading, spacing: HermesTheme.sectionSpacing) {
VStack(alignment: .leading, spacing: HermesTheme.sectionSpacing) {
HermesSectionHeader( HermesSectionHeader(
title: localization.string(for: "session.title"), title: localization.string(for: "session.title"),
subtitle: localization.string(for: "session.subtitle") subtitle: localization.string(for: "session.subtitle")
@@ -67,7 +66,7 @@ struct SessionView: View {
.foregroundStyle(HermesTheme.textSecondary) .foregroundStyle(HermesTheme.textSecondary)
} }
} }
} .hermesCard()
.onAppear { .onAppear {
analytics.track("screen_viewed", attributes: ["screen_name": "session"]) analytics.track("screen_viewed", attributes: ["screen_name": "session"])
} }
@@ -1,7 +1,45 @@
import SwiftUI import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject private var localization: LocalizationStore
var body: some View { 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)
}
} }
} }
@@ -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 = "<group>"; };
3686935FA33F0278440C2137 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/HermesErrorMapper.swift; sourceTree = "<group>"; };
DA1E3F27959B70E2FA168E86 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/HermesRepository.swift; sourceTree = "<group>"; };
6DE0108803DE783A7D5D528C = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RootView.swift; sourceTree = "<group>"; };
895DC0E4CCA3A72E5897D850 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Analytics/AnalyticsClient.swift; sourceTree = "<group>"; };
A24F440D4AC95A5E5A294BB5 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/DesignSystem/Theme.swift; sourceTree = "<group>"; };
E727CD23C8DFCD3FFA972118 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Gestures/GestureHandlers.swift; sourceTree = "<group>"; };
09BEEF1EACD1683731EA8B13 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Haptics/HapticsController.swift; sourceTree = "<group>"; };
618EEBF90A2888756D2A0D3D = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Localization/LocalizationStore.swift; sourceTree = "<group>"; };
3C969AD72CFEAAFD87172BC5 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Media/HermesVideoPlayerView.swift; sourceTree = "<group>"; };
06986680EEC52DA119F3C422 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Media/PlayerCoordinator.swift; sourceTree = "<group>"; };
154F342749182EA88D7D3EA2 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Networking/APIClient.swift; sourceTree = "<group>"; };
2927929745A862DD7DB483BE = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core/Networking/APIModels.swift; sourceTree = "<group>"; };
738C6476A9F7A2BBCA018EC7 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Feed/FeedView.swift; sourceTree = "<group>"; };
A6E7323ECDEAA449DD2925F0 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Onboarding/OnboardingView.swift; sourceTree = "<group>"; };
29BBCB2FC555E6C905C380A7 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Result/ResultView.swift; sourceTree = "<group>"; };
D34D3C5C813EAB2AD265F9CE = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Reveal/RevealView.swift; sourceTree = "<group>"; };
16E0F69DFE5FC784FB2BBECA = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Round/RoundView.swift; sourceTree = "<group>"; };
78ADAFCECDB66900EB275E3D = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Selection/SelectionView.swift; sourceTree = "<group>"; };
B9C59E00C07DD533A7DC8126 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Session/SessionView.swift; sourceTree = "<group>"; };
55B391E8147E161F76672032 = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features/Settings/SettingsView.swift; sourceTree = "<group>"; };
36A67B6FF0A52868D30E3AFF = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests/HermesAppTests/LocalizationStoreTests.swift; sourceTree = "<group>"; };
5E777846BC9ED35ED93034AA = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Resources/Localization/en.lproj/Localizable.strings; sourceTree = "<group>"; };
9710CA4A388CBE265276E667 = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Resources/Localization/sv.lproj/Localizable.strings; sourceTree = "<group>"; };
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 = "<group>"; };
1C48CE29E4E135EE62951B22 = {isa = PBXGroup; children = (
AFDCE723CFA31DF2A91DA374,
3686935FA33F0278440C2137,
DA1E3F27959B70E2FA168E86,
6DE0108803DE783A7D5D528C,
); path = App; sourceTree = "<group>"; };
410188F9DC6E499FEC68BCF6 = {isa = PBXGroup; children = (
895DC0E4CCA3A72E5897D850,
A24F440D4AC95A5E5A294BB5,
E727CD23C8DFCD3FFA972118,
09BEEF1EACD1683731EA8B13,
618EEBF90A2888756D2A0D3D,
3C969AD72CFEAAFD87172BC5,
06986680EEC52DA119F3C422,
154F342749182EA88D7D3EA2,
2927929745A862DD7DB483BE,
); path = Core; sourceTree = "<group>"; };
1F26A8704D8CDB03D5D3D4BB = {isa = PBXGroup; children = (
738C6476A9F7A2BBCA018EC7,
A6E7323ECDEAA449DD2925F0,
29BBCB2FC555E6C905C380A7,
D34D3C5C813EAB2AD265F9CE,
16E0F69DFE5FC784FB2BBECA,
78ADAFCECDB66900EB275E3D,
B9C59E00C07DD533A7DC8126,
55B391E8147E161F76672032,
); path = Features; sourceTree = "<group>"; };
A024F06580C79B7582C3806A = {isa = PBXGroup; children = (
5E777846BC9ED35ED93034AA,
9710CA4A388CBE265276E667,
); path = Resources; sourceTree = "<group>"; };
156052F04525D0F710673CF0 = {isa = PBXGroup; children = (
36A67B6FF0A52868D30E3AFF,
); path = Tests; sourceTree = "<group>"; };
E461FA3D1EC1D072C116F41B = {isa = PBXGroup; children = (
1C48CE29E4E135EE62951B22,
410188F9DC6E499FEC68BCF6,
1F26A8704D8CDB03D5D3D4BB,
A024F06580C79B7582C3806A,
156052F04525D0F710673CF0,
5151B46607EBC8890D8DFCC2,
); sourceTree = "<group>"; };
/* 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;
}
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme LastUpgradeVersion="1600" version="1.7">
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
<BuildActionEntries>
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="E9FB55C6F47F1FFA66FE0D94" BuildableName="HermesApp.app" BlueprintName="HermesApp" ReferencedContainer="container:HermesApp.xcodeproj"/>
</BuildActionEntry>
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="32127D1229D93E1AD0281CF5" BuildableName="HermesAppTests.xctest" BlueprintName="HermesAppTests" ReferencedContainer="container:HermesApp.xcodeproj"/>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
<Testables>
<TestableReference skipped="NO">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="32127D1229D93E1AD0281CF5" BuildableName="HermesAppTests.xctest" BlueprintName="HermesAppTests" ReferencedContainer="container:HermesApp.xcodeproj"/>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
<BuildableProductRunnable runnableDebuggingMode="0">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="E9FB55C6F47F1FFA66FE0D94" BuildableName="HermesApp.app" BlueprintName="HermesApp" ReferencedContainer="container:HermesApp.xcodeproj"/>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
<BuildableProductRunnable runnableDebuggingMode="0">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="E9FB55C6F47F1FFA66FE0D94" BuildableName="HermesApp.app" BlueprintName="HermesApp" ReferencedContainer="container:HermesApp.xcodeproj"/>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction buildConfiguration="Debug"/>
<ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
</Scheme>
@@ -56,3 +56,9 @@
"session.app_version_label" = "App version"; "session.app_version_label" = "App version";
"session.device_model_label" = "Device model"; "session.device_model_label" = "Device model";
"session.os_version_label" = "OS version"; "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";
@@ -56,3 +56,9 @@
"session.app_version_label" = "Appversion"; "session.app_version_label" = "Appversion";
"session.device_model_label" = "Enhetsmodell"; "session.device_model_label" = "Enhetsmodell";
"session.os_version_label" = "OS-version"; "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";
@@ -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)")
}
}
}