Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/app-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ concurrency:

jobs:
build:
runs-on: macos-15
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
Expand All @@ -48,9 +48,14 @@ jobs:
- scheme: Strand
destination: 'generic/platform=macOS'
extra_args: 'ARCHS="x86_64 arm64" ONLY_ACTIVE_ARCH=NO'
runner: macos-15
# iOS needs the iOS 26 SDK (Liquid Glass's `glassEffect`, landed 2026-07-07) which macos-15's
# default Xcode doesn't ship — matches the runner fork-testing-build.yml/fork-release.yml
# already use for their NOOPiOS legs. macos-15 stays correct for the macOS/Strand leg above.
- scheme: NOOPiOS
destination: 'generic/platform=iOS Simulator'
extra_args: ''
runner: macos-26
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,58 @@
}
}
},
"Auto": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Auto"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Auto"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Auto"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Auto"
}
},
"pt-PT": {
"stringUnit": {
"state": "translated",
"value": "Auto"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Авто"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "自动"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "自動"
}
}
}
},
"Awake": {
"localizations": {
"de": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,14 @@ final class DecoderOracleTests: XCTestCase {
let gy = parsed["gravity_y"]?.doubleValue
let gz = parsed["gravity_z"]?.doubleValue
XCTAssertNotNil(gx, "\(frame.name): gravity did not decode")
let mag = ((gx ?? 0) * (gx ?? 0) + (gy ?? 0) * (gy ?? 0) + (gz ?? 0) * (gz ?? 0)).squareRoot()
// Broken into named sub-expressions: the single-line nested-optional-coalescing form
// occasionally blew the type-checker's time budget in CI ("unable to type-check this
// expression in reasonable time"), a Swift compiler perf cliff, not a real ambiguity.
let x: Double = gx ?? 0
let y: Double = gy ?? 0
let z: Double = gz ?? 0
let sumSquares: Double = x * x + y * y + z * z
let mag = sumSquares.squareRoot()
XCTAssertEqual(mag, wantMag, accuracy: 0.1, "\(frame.name): |gravity|")
default:
switch expected {
Expand Down
178 changes: 178 additions & 0 deletions Strand/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -56216,6 +56216,52 @@
}
}
},
"HRV window": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "HRV-Fenster"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Ventana de VFC"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Fenêtre VFC"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Finestra HRV"
}
},
"pt-PT": {
"stringUnit": {
"state": "translated",
"value": "Janela de VFC"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "HRV 窗口"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "HRV 窗口"
}
}
}
},
"HRV ↔ Charge": {
"localizations": {
"de": {
Expand Down Expand Up @@ -79013,6 +79059,52 @@
}
}
},
"Night": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Nacht"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Noche"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Nuit"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Notte"
}
},
"pt-PT": {
"stringUnit": {
"state": "translated",
"value": "Noite"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "夜间"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "夜間"
}
}
}
},
"Night detail": {
"localizations": {
"de": {
Expand Down Expand Up @@ -87933,6 +88025,52 @@
}
}
},
"Optional. Add a photo for the avatar in the top-left. Stored only on this %@. NOOP is offline, so it's never uploaded.": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Optional. Füge ein Foto für den Avatar oben links hinzu. Nur auf diesem %@ gespeichert. NOOP ist offline, es wird also nie hochgeladen."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Opcional. Añade una foto para el avatar de la esquina superior izquierda. Se guarda solo en este %@. NOOP funciona sin conexión, así que nunca se sube."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Facultatif. Ajoutez une photo pour l'avatar en haut à gauche. Stockée uniquement sur ce %@. NOOP est hors ligne, elle n'est donc jamais mise en ligne."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Facoltativo. Aggiungi una foto per l'avatar in alto a sinistra. Memorizzata solo su questo %@, NOOP è offline, quindi non viene mai caricata."
}
},
"pt-PT": {
"stringUnit": {
"state": "needs_review",
"value": "Opcional. Añade uma foto para el avatar da esquina superior izquierda. Se guarda só neste %@. NOOP funciona sem ligação, así que nunca se sobe."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "可选。为左上角的头像添加一张照片。仅存储在此 %@ 上,NOOP 是离线的,因此绝不会上传。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "可選。為左上角的頭像新增一張照片。僅儲存在此 %@ 上,NOOP 是離線的,因此絕不會上傳。"
}
}
}
},
"Optional. Stored only on this device.": {
"localizations": {
"de": {
Expand Down Expand Up @@ -117261,6 +117399,46 @@
}
}
},
"System": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "System"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Sistema"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Système"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Sistema"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "系统"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "系統"
}
}
}
},
"System follows your %@'s light or dark setting.": {
"localizations": {
"de": {
Expand Down
22 changes: 16 additions & 6 deletions Strand/Screens/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,18 @@ struct SettingsView: View {
/// iOS 16+ and macOS 13+ (NOOP's floor), so the same control serves both platforms — no
/// availability gating needed. The photo is stored only on this device (NOOP is fully offline).
private var profilePhotoCard: some View {
SettingsSection(
// The blurb interpolates `Platform.deviceNounPhrase`, itself an already-resolved localized
// String ("this iPhone" / "dieses iPhone"). Interpolating a resolved String directly into a
// `LocalizedStringKey` literal (the `blurb:` parameter's inferred type) confused SwiftUI's
// text-measurement pass: the blurb rendered with zero trailing margin, running edge-to-edge
// instead of wrapping inside the card's padding (#left/right edge clipping). Resolving the
// whole sentence through `String(localized:)` first, then handing SwiftUI the plain String via
// `LocalizedStringKey(_:)`, sidesteps the nested-nested-localization measurement bug.
let blurbText = String(localized: "Optional. Add a photo for the avatar in the top-left. Stored only on this \(Platform.deviceNoun). NOOP is offline, so it's never uploaded.")
return SettingsSection(
icon: "person.crop.circle",
title: "Profile photo",
blurb: "Optional. Add a photo for the avatar in the top-left. Stored only on \(Platform.deviceNounPhrase). NOOP is offline, so it's never uploaded."
blurb: LocalizedStringKey(blurbText)
) {
HStack(spacing: 16) {
ProfileAvatarView(imageData: profile.avatarImageData, size: 64)
Expand All @@ -270,7 +278,6 @@ struct SettingsView: View {
.accessibilityHint("Reverts to the default profile icon")
}
}
.frame(maxWidth: .infinity)
}
}
// Load the picked photo's bytes, then hand them to the store (which downscales + persists).
Expand Down Expand Up @@ -606,8 +613,8 @@ struct SettingsView: View {
}
rowDivider
FormRow(label: "Temperature") {
// Three-way: "Match" follows the system above; °C / °F pin it explicitly. Stored as
// an empty string ("match") or the TemperatureUnit raw value.
// Three-way: "Match" follows the measurement system above; °C / °F pin it explicitly.
// Stored as an empty string ("match") or the TemperatureUnit raw value.
Picker("Temperature", selection: $temperatureRaw) {
Text("Match").tag("")
Text("°C").tag(TemperatureUnit.celsius.rawValue)
Expand Down Expand Up @@ -883,8 +890,11 @@ struct SettingsView: View {
// WHOOP/Polar). Unlike the Effort scale this CHANGES the number, so a switch re-scores +
// re-baselines (like a sleep edit).
FormRow(label: "HRV window") {
// "Night" (not "Whole night"/"Overnight") — a single short word so the two-segment
// control fits without truncating once it sizes to the row (some locales' translations
// of longer phrases still overflowed), same fix as the Temperature/Theme pickers above.
Picker("HRV window", selection: $hrvWindowRaw) {
Text("Whole night").tag(HrvWindow.whole.rawValue)
Text("Night").tag(HrvWindow.whole.rawValue)
Text("Deep sleep").tag(HrvWindow.deep.rawValue)
}
.labelsHidden()
Expand Down
Loading