فهرست منبع

Use dev Localizable.xcstrings

Sam King 1 سال پیش
والد
کامیت
b3df7479c1
98فایلهای تغییر یافته به همراه25371 افزوده شده و 21281 حذف شده
  1. 1 1
      .github/ISSUE_TEMPLATE/config.yml
  2. 1 1
      .github/workflows/add_to_project.yml
  3. 106 0
      .github/workflows/auto_version_dev.yml
  4. 1 1
      .github/workflows/stale_issues.yml
  5. 1 1
      CODE_OF_CONDUCT.md
  6. 2 1
      Config.xcconfig
  7. 7 7
      Model/CoreDataStack.swift
  8. 2 2
      PRIVACY_POLICY.md
  9. 35 32
      README.md
  10. 8 8
      Trio Watch App Extension/WatchState+Requests.swift
  11. 2 2
      Trio Watch App Extension/WatchState.swift
  12. 15 7
      Trio.xcodeproj/project.pbxproj
  13. 2 0
      Trio/Resources/Info.plist
  14. 10 10
      Trio/Sources/APS/APSManager.swift
  15. 5 5
      Trio/Sources/APS/CGM/BluetoothTransmitter.swift
  16. 3 3
      Trio/Sources/APS/DeviceDataManager.swift
  17. 7 0
      Trio/Sources/APS/Extensions/DecimalExtensions.swift
  18. 4 4
      Trio/Sources/APS/FetchGlucoseManager.swift
  19. 1 1
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  20. 1 1
      Trio/Sources/APS/OpenAPS/Script.swift
  21. 19 11
      Trio/Sources/APS/Storage/CarbsStorage.swift
  22. 1 1
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  23. 4 4
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  24. 3 3
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  25. 1 1
      Trio/Sources/APS/Storage/OverrideStorage.swift
  26. 1 1
      Trio/Sources/APS/Storage/TDDStorage.swift
  27. 5 4
      Trio/Sources/Application/AppDelegate.swift
  28. 22 2
      Trio/Sources/Application/TrioApp.swift
  29. 2 2
      Trio/Sources/Helpers/MainChartHelper.swift
  30. 3 3
      Trio/Sources/Helpers/PropertyWrappers/PersistedProperty.swift
  31. 42 0
      Trio/Sources/Helpers/TimeAgoFormatter.swift
  32. 24410 20554
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  33. 35 1
      Trio/Sources/Logger/Logger.swift
  34. 1 1
      Trio/Sources/Models/DecimalPickerSettings.swift
  35. 6 11
      Trio/Sources/Models/TrioSettings.swift
  36. 9 9
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  37. 6 6
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  38. 1 1
      Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  39. 6 6
      Trio/Sources/Modules/AppDiagnostics/View/AppDiagnosticsRootView.swift
  40. 0 233
      Trio/Sources/Modules/AppDiagnostics/View/PrivacyPolicyView.swift
  41. 2 2
      Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  42. 1 1
      Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  43. 1 1
      Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  44. 1 1
      Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift
  45. 3 1
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  46. 3 1
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  47. 7 7
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  48. 1 1
      Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  49. 1 1
      Trio/Sources/Modules/Home/HomeStateModel+Setup/CurrentTDDSetup.swift
  50. 2 2
      Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  51. 1 1
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  52. 1 1
      Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  53. 4 4
      Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  54. 3 3
      Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  55. 2 2
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  56. 1 1
      Trio/Sources/Modules/Home/HomeStateModel.swift
  57. 1 11
      Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  58. 2 6
      Trio/Sources/Modules/Home/View/Header/LoopView.swift
  59. 18 1
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  60. 1 1
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  61. 1 1
      Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  62. 5 0
      Trio/Sources/Modules/Main/MainDataFlow.swift
  63. 10 16
      Trio/Sources/Modules/Main/MainStateModel.swift
  64. 5 4
      Trio/Sources/Modules/Main/View/MainRootView.swift
  65. 13 24
      Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  66. 3 3
      Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  67. 0 3
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  68. 10 18
      Trio/Sources/Modules/Onboarding/OnboardingStateModel+Nightscout.swift
  69. 49 16
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  70. 25 4
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BluetoothPermissionStepView.swift
  71. 6 5
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DiagnosticsStepView.swift
  72. 15 16
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutImportStepView.swift
  73. 21 6
      Trio/Sources/Modules/Settings/SettingItems.swift
  74. 17 20
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  75. 1 1
      Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift
  76. 1 1
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  77. 1 1
      Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift
  78. 2 2
      Trio/Sources/Modules/Stat/StatStateModel.swift
  79. 1 1
      Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift
  80. 110 23
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  81. 5 2
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  82. 2 2
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  83. 1 1
      Trio/Sources/Services/Calendar/CalendarManager.swift
  84. 7 7
      Trio/Sources/Services/HealthKit/HealthKitManager.swift
  85. 27 10
      Trio/Sources/Services/LiveActivity/LiveActivityManager.swift
  86. 9 9
      Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  87. 24 36
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  88. 4 4
      Trio/Sources/Services/Network/TidepoolManager.swift
  89. 3 3
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift
  90. 1 1
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift
  91. 11 11
      Trio/Sources/Services/Storage/FileStorage.swift
  92. 1 5
      Trio/Sources/Services/UnlockManager/UnlockManager.swift
  93. 1 1
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  94. 42 21
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  95. 5 5
      Trio/Sources/Services/WatchManager/GarminManager.swift
  96. 4 4
      Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  97. 4 4
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift
  98. 77 0
      TrioTests/LocalizationTests.swift

+ 1 - 1
.github/ISSUE_TEMPLATE/config.yml

@@ -1,5 +1,5 @@
 blank_issues_enabled: false
 contact_links:
   - name: "🆘 Individual troubleshooting help: Please go to the Discord Trio Server"
-    url: https://discord.com/invite/FnwFEFUwXE
+    url: https://discord.triodocs.org
     about: Are you having an issue with your individual setup? Please first go to the Discord Trio Server and post there, with details of your setup (App version, pump, CGM, and CGM app) and the issue you are observing

+ 1 - 1
.github/workflows/add_to_project.yml

@@ -1,4 +1,4 @@
-name: 8. DONT RUN Add bugs to bugs project
+name: zzz [DO NOT RUN] Add Bugs to Project 'Bugs'
 
 on:
   issues:

+ 106 - 0
.github/workflows/auto_version_dev.yml

@@ -0,0 +1,106 @@
+# -----------------------------------------------------------------------------
+# Workflow: `auto_version_dev.yml`
+#
+# Description:
+# This GitHub Actions workflow automatically manages and increments the
+# `APP_DEV_VERSION` defined in `Config.xcconfig` on every push to `dev` branch.
+# This version is used for internal tracking and diagnostics (e.g. in
+# Crashlytics) and follows a 4-digit semantic versioning format:
+# `MAJOR.MINOR.PATCH.FEATURE`.
+#
+# Versioning Logic:
+# - Reads the base version from `APP_VERSION = x.y.z`
+# - Reads the last internal dev version from `APP_DEV_VERSION`
+#
+# Behavior:
+# - If `APP_DEV_VERSION` matches `APP_VERSION` (e.g. both are `0.5.0`),
+#   it assumes the first dev push after a release and sets `APP_DEV_VERSION`
+#   to `APP_VERSION.1` (e.g. `0.5.0.1`)
+# - If `APP_DEV_VERSION` is already in 4-digit form (e.g. `0.5.0.3`),
+#   it increments the fourth digit (e.g. → `0.5.0.4`)
+#
+# Example Progression:
+# - Release sets `APP_VERSION = 0.5.0`, `APP_DEV_VERSION = 0.5.0`
+# - First push to `dev`:      → `APP_DEV_VERSION = 0.5.0.1`
+# - Second push to `dev`:     → `APP_DEV_VERSION = 0.5.0.2`
+# - ...
+#
+# Commit Handling:
+# The updated value is committed and pushed back to the `dev` branch.
+# - The bump commit includes the `[skip ci]` tag in its message
+# - This prevents the workflow from re-triggering itself in a loop
+# 
+#
+# Prerequisites:
+# - `APP_VERSION` must be present in `Config.xcconfig` in the form `x.y.z`
+# - `APP_DEV_VERSION` must either match `APP_VERSION` or be `x.y.z.w`
+# - GitHub Actions must have write permission to push to `dev`
+# - This workflow only runs when the repository owner is `nightscout`
+# -----------------------------------------------------------------------------
+
+name: zzz [DO NOT RUN] Bump APP_DEV_VERSION on dev push
+
+on:
+  push:
+    branches:
+      - dev
+
+jobs:
+  bump-dev-version:
+    if: github.repository_owner == 'nightscout'
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v4
+        with:
+         token: ${{ secrets.TRIO_TOKEN_AUTOBUMP }}
+
+      - name: Set up Git
+        run: |
+          git config --global user.name "github-actions[bot]"
+          git config --global user.email "github-actions[bot]@users.noreply.github.com"
+
+      - name: Bump APP_DEV_VERSION
+        run: |
+          FILE=Config.xcconfig
+
+          # Read current APP_VERSION
+          BASE_VERSION=$(grep '^APP_VERSION' "$FILE" | cut -d '=' -f2 | xargs)
+
+          # Read existing APP_DEV_VERSION, if any
+          DEV_LINE=$(grep '^APP_DEV_VERSION' "$FILE" || echo "")
+          if [ -z "$DEV_LINE" ]; then
+            CURRENT_DEV_VERSION="$BASE_VERSION"
+          else
+            CURRENT_DEV_VERSION=$(echo "$DEV_LINE" | cut -d '=' -f2 | xargs)
+          fi
+
+          echo "APP_VERSION       = $BASE_VERSION"
+          echo "APP_DEV_VERSION   = $CURRENT_DEV_VERSION"
+
+          # Decide next dev version
+          if [ "$CURRENT_DEV_VERSION" = "$BASE_VERSION" ]; then
+            # First post-release commit to dev → bump to .1
+            NEW_DEV_VERSION="${BASE_VERSION}.1"
+            if [ -z "$DEV_LINE" ]; then
+              echo "APP_DEV_VERSION = $NEW_DEV_VERSION" >> "$FILE"
+            else
+              sed -i -E "s|^APP_DEV_VERSION *= *.*|APP_DEV_VERSION = $NEW_DEV_VERSION|" "$FILE"
+            fi
+          else
+            # Already in .X form → bump last digit
+            IFS='.' read -r MAJOR MINOR PATCH FEATURE <<< "$CURRENT_DEV_VERSION"
+            FEATURE=$((FEATURE + 1))
+            NEW_DEV_VERSION="$MAJOR.$MINOR.$PATCH.$FEATURE"
+            sed -i -E "s|^APP_DEV_VERSION *= *.*|APP_DEV_VERSION = $NEW_DEV_VERSION|" "$FILE"
+          fi
+
+          echo "NEW APP_DEV_VERSION = $NEW_DEV_VERSION"
+          echo "NEW_DEV_VERSION=$NEW_DEV_VERSION" >> $GITHUB_ENV
+
+      - name: Commit and push updated dev version
+        run: |
+          git add Config.xcconfig
+          git commit -m "CI: Bump APP_DEV_VERSION to $NEW_DEV_VERSION [skip ci]"
+          git push

+ 1 - 1
.github/workflows/stale_issues.yml

@@ -1,4 +1,4 @@
-name: 8. DONT RUN close inactive issues
+name: zzz [DO NOT RUN] Close Inactive Issues
 on:
   schedule:
     - cron: "30 1 * * *"

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -59,7 +59,7 @@ representative at an online or offline event.
 ## Enforcement
 
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the Discord server admins. Please join our [Discord server](http://discord.diy-trio.org) to contact
+reported to the Discord server admins. Please join our [Discord server](http://discord.triodocs.org) to contact
 them directly for any enforcement issues. All complaints will be reviewed and
 investigated promptly and fairly.
 

+ 2 - 1
Config.xcconfig

@@ -1,5 +1,6 @@
 APP_DISPLAY_NAME = Trio
-APP_VERSION = 0.4.0
+APP_VERSION = 0.5.0
+APP_DEV_VERSION = 0.5.0.15
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##

+ 7 - 7
Model/CoreDataStack.swift

@@ -114,7 +114,7 @@ class CoreDataStack: ObservableObject {
         do {
             try await fetchPersistentHistoryTransactionsAndChanges()
         } catch {
-            debug(.coreData, "\(error.localizedDescription)")
+            debug(.coreData, "\(error)")
         }
     }
 
@@ -162,7 +162,7 @@ class CoreDataStack: ObservableObject {
             } catch {
                 debug(
                     .coreData,
-                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history from before \(date): \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history from before \(date): \(error)"
                 )
             }
         }
@@ -194,7 +194,7 @@ class CoreDataStack: ObservableObject {
         try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
             persistentContainer.loadPersistentStores { storeDescription, error in
                 if let error = error {
-                    warning(.coreData, "Failed to load persistent stores: \(error.localizedDescription)")
+                    warning(.coreData, "Failed to load persistent stores: \(error)")
                     continuation.resume(throwing: error)
                 } else {
                     debug(.coreData, "Successfully loaded persistent store: \(storeDescription.url?.absoluteString ?? "unknown")")
@@ -242,7 +242,7 @@ class CoreDataStack: ObservableObject {
             debug(.coreData, "Core Data stack initialized successfully")
 
         } catch {
-            debug(.coreData, "Failed to initialize Core Data stack: \(error.localizedDescription)")
+            debug(.coreData, "Failed to initialize Core Data stack: \(error)")
 
             // If we still have retries left, try again after a delay
             if retryCount < maxRetries {
@@ -280,7 +280,7 @@ extension CoreDataStack {
                 try viewContext.save()
                 debug(.coreData, "Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
             } catch {
-                debug(.coreData, "Failed to delete data: \(error.localizedDescription)")
+                debug(.coreData, "Failed to delete data: \(error)")
             }
         }
     }
@@ -339,7 +339,7 @@ extension CoreDataStack {
 
             debug(.coreData, "Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
         } catch {
-            debug(.coreData, "Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            debug(.coreData, "Failed to fetch or delete data: \(error) \(DebuggingIdentifiers.failed)")
             throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
     }
@@ -406,7 +406,7 @@ extension CoreDataStack {
                 "Successfully deleted \(childType) data related to \(parentType) objects older than \(days) days. \(DebuggingIdentifiers.succeeded)"
             )
         } catch {
-            debug(.coreData, "Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            debug(.coreData, "Failed to fetch or delete data: \(error) \(DebuggingIdentifiers.failed)")
             throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
     }

+ 2 - 2
PRIVACY_POLICY.md

@@ -111,9 +111,9 @@ updating the "Last Updated" date.
 ## Contact Us
 
 If you have any questions about this Privacy Policy, please contact us
-on [Discord](http://discord.diy-trio.org/) or send us an email at
+on [Discord](http://discord.triodocs.org/) or send us an email at
 trio.diy.diabetes@gmail.com.
 
 ## Last Updated
 
-April 15, 2025
+May 14, 2025

+ 35 - 32
README.md

@@ -16,7 +16,7 @@ You can either use the Build Script or you can run each command manually.
 
 ### Build Script:
 
-If you copy, paste, and run the following script in Terminal, it will guide you through downloading and installing Trio. More information about the script can be found [here](https://docs.diy-trio.org/operate/build/#build-trio-with-script).
+If you copy, paste, and run the following script in Terminal, it will guide you through downloading and installing Trio. More information about the script can be found [here](https://triodocs.org/0.2.x/operate/build/#build-trio-with-script).
 
 ```
 /bin/bash -c "$(curl -fsSL \
@@ -34,69 +34,72 @@ git clone --branch=<branch> --recurse-submodules https://github.com/nightscout/T
 Create a ConfigOverride.xcconfig file that contains your Apple Developer ID (something like `123A4BCDE5`). This will automate signing of the build targets in Xcode:
 
 Copy the command below, and replace `xxxxxxxxxx` by your Apple Developer ID before running the command in Terminal.
+
 ```
 echo 'DEVELOPER_TEAM = xxxxxxxxxx' > ConfigOverride.xcconfig
 ```
 
 Then launch Xcode and build the Trio app:
+
 ```
 xed .
 ```
 
 ## To build directly in GitHub, without using Xcode:
 
-Instructions:
+**Instructions**:
 
-For main branch:
-* https://github.com/nightscout/Trio/blob/main/fastlane/testflight.md   
+- For **`main`** branch:  
+   https://github.com/nightscout/Trio/blob/main/fastlane/testflight.md
+- For **`dev`** branch:
+  https://github.com/nightscout/Trio/blob/dev/fastlane/testflight.md
 
-For dev branch:
-* https://github.com/nightscout/Trio/blob/dev/fastlane/testflight.md   
+Instructions in **greater detail**, but **not Trio-specific**:
 
-Instructions in greater detail, but not Trio-specific:  
-* https://loopkit.github.io/loopdocs/gh-actions/gh-overview/
+- https://loopkit.github.io/loopdocs/gh-actions/gh-overview/
 
 ## Please understand that Trio is:
+
 - an open-source system developed by enthusiasts and for use at your own risk
 - not CE or FDA approved for therapy.
 
+## Documentation
 
-# Documentation
-
-[Discord Trio - Server ](http://discord.diy-trio.org)
-
-[Trio documentation](https://docs.diy-trio.org/)
+- [Discord Trio - Server ](https://discord.triodocs.org/)
+- [Trio documentation](https://triodocs.org/)
+- [OpenAPS documentation](https://openaps.readthedocs.io/en/latest/)
+- [Crowdin](https://crowdin.triodocs.org/) is the collaborative platform we are using to manage the **translation** and localization of the Trio App.
+<!--   TODO: Add status graphic for the Crowdin Project -->
 
-TODO: Add link: Trio Website (under development, not existing yet)
+## Support
 
-[OpenAPS documentation](https://openaps.readthedocs.io/en/latest/)
+- [Trio Facebook Group](https://facebook.triodocs.org/)
+- [Loop and Learn Facebook Group](https://m.facebook.com/groups/LOOPandLEARN/)
+- [Looped Facebook Group](https://m.facebook.com/groups/TheLoopedGroup/)
 
-TODO: Add link and status graphic: Crowdin Project for translation of Trio (not existing yet)
+## Contribute
 
-# Support
-
-[Trio Facebook Group](https://m.facebook.com/groups/1351938092206709/)
+If you would like to give something back to the Trio community, there are several ways to contribute:
 
-[Loop and Learn Facebook Group](https://m.facebook.com/groups/LOOPandLEARN/)
+- **Help others**: assist users by answering questions and guiding them in support communities.
+- Improve the **documentation**: update or expand TrioDocs to help users build and use Trio.
+- Improve the **app**: contribute **code**, features, or fixes to the Trio iOS app.
 
-[Looped Facebook Group](https://m.facebook.com/groups/TheLoopedGroup/)
+### Pay it forward
 
-# Contribute
+When you have successfully built Trio and managed to get it working well for your diabetes management, it's time to pay it forward.
+You can start by **responding to questions** in the **Facebook or Discord** support groups, **helping others** make the best out of Trio.
 
-If you would like to give something back to the Trio community, there are several ways to contribute:
+### Translate
 
-## Pay it forward
-When you have successfully built Trio and managed to get it working well for your diabetes management, it's time to pay it forward. 
-You can start by responding to questions in the Facebook or Discord support groups, helping others make the best out of Trio.
-
-## Translate
-Trio is translated into several languages to make sure it's easy to understand and use all over the world. 
-Translation is done using [Crowdin](https://crowdin.com/project/trio), and does not require any programming skills.
+Trio is translated into several languages to make sure it's easy to understand and use all over the world.
+Translation is done using [Crowdin](https://crowdin.triodocs.org/), and does not require any programming skills.
 If your preferred language is missing or you'd like to improve the translation, please sign up as a translator on [Crowdin](https://crowdin.com/project/trio).
 
-## Develop
+### Develop
+
 Do you speak JS or Swift? Do you have UI/UX skills? Do you know how to optimize API calls or improve data storage? Do you have experience with testing and release management?
 Trio is a collaborative project. We always welcome fellow enthusiasts who can contribute with new code, UI/UX improvements, code reviews, testing and release management.
 If you want to contribute to the development of Trio, please reach out on Discord or Facebook.
 
-For questions or contributions, please join our [Discord server](https://discord.gg/KepAG6RdYZ).
+For questions or contributions, please join our [Discord server](https://discord.triodocs.org).

+ 8 - 8
Trio Watch App Extension/WatchState+Requests.swift

@@ -25,7 +25,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("Error sending bolus request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("Error sending bolus request: \(error)")
             }
         }
 
@@ -59,7 +59,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("Error sending carbs request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("Error sending carbs request: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }
@@ -91,7 +91,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("⌚️ Error sending cancel override request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Error sending cancel override request: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }
@@ -124,7 +124,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("⌚️ Error sending activate override request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Error sending activate override request: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }
@@ -156,7 +156,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Error sending cancel temp target request: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }
@@ -189,7 +189,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ Error sending activate temp target request: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }
@@ -222,7 +222,7 @@ extension WatchState {
 
         session.sendMessage(message, replyHandler: nil) { error in
             Task {
-                await WatchLogger.shared.log("Error requesting bolus recommendation: \(error.localizedDescription)")
+                await WatchLogger.shared.log("Error requesting bolus recommendation: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }
@@ -254,7 +254,7 @@ extension WatchState {
 
             session.sendMessage(message, replyHandler: nil) { error in
                 Task {
-                    await WatchLogger.shared.log("⌚️ Error requesting WatchState update: \(error.localizedDescription)")
+                    await WatchLogger.shared.log("⌚️ Error requesting WatchState update: \(error)")
                     await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                     await WatchLogger.shared.persistLogsLocally()
                 }

+ 2 - 2
Trio Watch App Extension/WatchState.swift

@@ -151,7 +151,7 @@ import WatchConnectivity
         DispatchQueue.main.async {
             if let error = error {
                 Task {
-                    await WatchLogger.shared.log("⌚️ Watch session activation failed: \(error.localizedDescription)", force: true)
+                    await WatchLogger.shared.log("⌚️ Watch session activation failed: \(error)", force: true)
                     await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                     await WatchLogger.shared.persistLogsLocally()
                 }
@@ -272,7 +272,7 @@ import WatchConnectivity
     func session(_: WCSession, didFinish _: WCSessionUserInfoTransfer, error: (any Error)?) {
         if let error = error {
             Task {
-                await WatchLogger.shared.log("⌚️ transferUserInfo failed with error: \(error.localizedDescription)")
+                await WatchLogger.shared.log("⌚️ transferUserInfo failed with error: \(error)")
                 await WatchLogger.shared.log("⌚️ Saving logs to disk as fallback!")
                 await WatchLogger.shared.persistLogsLocally()
             }

+ 15 - 7
Trio.xcodeproj/project.pbxproj

@@ -281,6 +281,7 @@
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
+		3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		3BC0AA3B2DA74C87000DF7B7 /* iob-total.js in Resources */ = {isa = PBXBuildFile; fileRef = 3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */; };
@@ -670,12 +671,12 @@
 		DDA9AC092D672CF100E6F1A9 /* AppVersionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9AC082D672CEB00E6F1A9 /* AppVersionChecker.swift */; };
 		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
-		DDB0E3712DB087B6004B826F /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0E3702DB087B6004B826F /* PrivacyPolicyView.swift */; };
 		DDB0E3742DB1BAC1004B826F /* LogoBurstSplash.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */; };
 		DDC38E102D9B377800ADCB46 /* OnboardingView+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */; };
+		DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */; };
 		DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */; };
 		DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -686,6 +687,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
+		DDD5889D2DDDC9A900C8848D /* TimeAgoFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */; };
 		DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */; };
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
@@ -1150,6 +1152,7 @@
 		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
 		3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporterTests.swift; sourceTree = "<group>"; };
 		3B997DD12DC02AEF006B6BB2 /* glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = glucose.json; sourceTree = "<group>"; };
+		3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalExtensions.swift; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BC0AA3A2DA74C87000DF7B7 /* iob-total.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "iob-total.js"; sourceTree = "<group>"; };
@@ -1543,7 +1546,6 @@
 		DDA9AC0A2D678DAD00E6F1A9 /* blacklisted-versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blacklisted-versions.json"; sourceTree = "<group>"; };
 		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
 		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
-		DDB0E3702DB087B6004B826F /* PrivacyPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyView.swift; sourceTree = "<group>"; };
 		DDB0E3732DB1BAC1004B826F /* LogoBurstSplash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoBurstSplash.swift; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
@@ -1551,6 +1553,7 @@
 		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
 		DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewStepView.swift; sourceTree = "<group>"; };
 		DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Util.swift"; sourceTree = "<group>"; };
+		DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
 		DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingsUtil.swift; sourceTree = "<group>"; };
 		DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmodulesView.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
@@ -1561,6 +1564,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
+		DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoFormatter.swift; sourceTree = "<group>"; };
 		DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedA1cDisplayUnit.swift; sourceTree = "<group>"; };
 		DDD78A902DC4064800AC63F3 /* carbhistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = carbhistory.json; sourceTree = "<group>"; };
 		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
@@ -2504,6 +2508,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
 				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
@@ -2564,13 +2569,14 @@
 		38A504F625DDA0E200C5B9E8 /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */,
+				3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */,
 				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
-				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
-				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
 				CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */,
 				CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */,
-				CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */,
+				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
+				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 			);
 			path = Extensions;
 			sourceTree = "<group>";
@@ -2679,6 +2685,7 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
@@ -3829,7 +3836,6 @@
 		DDF690FF2DA2CA03008BF16C /* View */ = {
 			isa = PBXGroup;
 			children = (
-				DDB0E3702DB087B6004B826F /* PrivacyPolicyView.swift */,
 				DDF691062DA2CA28008BF16C /* AppDiagnosticsRootView.swift */,
 			);
 			path = View;
@@ -4394,7 +4400,6 @@
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */,
-				DDB0E3712DB087B6004B826F /* PrivacyPolicyView.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				3BCE75B32D4B38AE009E9453 /* InsulinSensitivities+Convert.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
@@ -4455,6 +4460,7 @@
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
+				DDD5889D2DDDC9A900C8848D /* TimeAgoFormatter.swift in Sources */,
 				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				3B5CD2B72D4AEA6600CE213C /* ComputedBGTargets.swift in Sources */,
 				3B5CD2B82D4AEA6600CE213C /* ComputedInsulinSensitivities.swift in Sources */,
@@ -4863,6 +4869,7 @@
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
+				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
@@ -4972,6 +4979,7 @@
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
+				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,
 				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,

+ 2 - 0
Trio/Resources/Info.plist

@@ -4,6 +4,8 @@
 <dict>
 	<key>AppGroupID</key>
 	<string>$(APP_GROUP_ID)</string>
+	<key>AppDevVersion</key>
+	<string>$(APP_DEV_VERSION)</string>
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
 	<array>
 		<string>$(PRODUCT_BUNDLE_IDENTIFIER).background-task.critical-event-log</string>

+ 10 - 10
Trio/Sources/APS/APSManager.swift

@@ -144,7 +144,7 @@ final class BaseAPSManager: APSManager, Injectable {
                     } catch {
                         debug(
                             .apsManager,
-                            "\(DebuggingIdentifiers.failed) Error creating profiles: \(error.localizedDescription)"
+                            "\(DebuggingIdentifiers.failed) Error creating profiles: \(error)"
                         )
                     }
                 }
@@ -226,7 +226,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 updatedStats.duration = roundDouble((updatedStats.end! - updatedStats.start).timeInterval / 60, 2)
                 updatedStats.loopStatus = error.localizedDescription
                 await loopCompleted(error: error, loopStatRecord: updatedStats)
-                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to complete Loop: \(error.localizedDescription)")
+                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to complete Loop: \(error)")
             }
 
             // Cleanup background task
@@ -332,7 +332,7 @@ final class BaseAPSManager: APSManager, Injectable {
         isLooping.send(false)
 
         if let error = error {
-            warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
+            warning(.apsManager, "Loop failed with error: \(error)")
             if let backgroundTask = backgroundTaskID {
                 await UIApplication.shared.endBackgroundTask(backgroundTask)
                 backgroundTaskID = .invalid
@@ -548,7 +548,7 @@ final class BaseAPSManager: APSManager, Injectable {
             bolusProgress.send(0)
             callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
         } catch {
-            warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
+            warning(.apsManager, "Bolus failed with error: \(error)")
             processError(APSError.pumpError(error))
             if !isSMB {
                 // Use MainActor to handle broadcaster notification
@@ -574,7 +574,7 @@ final class BaseAPSManager: APSManager, Injectable {
             debug(.apsManager, "Bolus cancelled")
             callback?(true, String(localized: "Bolus cancelled successfully.", comment: "Success message for canceling a bolus"))
         } catch {
-            debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
+            debug(.apsManager, "Bolus cancellation failed with error: \(error)")
             processError(APSError.pumpError(error))
             callback?(
                 false,
@@ -611,7 +611,7 @@ final class BaseAPSManager: APSManager, Injectable {
             try await pump.enactTempBasal(unitsPerHour: roundedAmout, for: duration)
             debug(.apsManager, "Temp Basal succeeded")
         } catch {
-            debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
+            debug(.apsManager, "Temp Basal failed with error: \(error)")
             processError(APSError.pumpError(error))
         }
     }
@@ -738,7 +738,7 @@ final class BaseAPSManager: APSManager, Injectable {
         } catch {
             debug(
                 .apsManager,
-                "\(DebuggingIdentifiers.failed) Error reporting enacted status: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error reporting enacted status: \(error)"
             )
         }
     }
@@ -1130,7 +1130,7 @@ final class BaseAPSManager: APSManager, Injectable {
         } catch {
             debug(
                 .apsManager,
-                "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error)"
             )
             return nil
         }
@@ -1155,7 +1155,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func processError(_ error: Error) {
-        warning(.apsManager, "\(error.localizedDescription)")
+        warning(.apsManager, "\(error)")
         lastError.send(error)
     }
 
@@ -1275,7 +1275,7 @@ extension BaseAPSManager: PumpManagerStatusObserver {
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
             } catch {
-                print("Failed to fetch or save battery: \(error.localizedDescription)")
+                debug(.apsManager, "Failed to fetch or save battery: \(error)")
             }
         }
         // TODO: - remove this after ensuring that NS still gets the same infos from Core Data

+ 5 - 5
Trio/Sources/APS/CGM/BluetoothTransmitter.swift

@@ -230,7 +230,7 @@ class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDele
         if let error = error {
             debug(
                 .deviceManager,
-                "failed to connect, for peripheral with name \(peripheral.name ?? "'unknown'"), with error: \(error.localizedDescription), will try again"
+                "failed to connect, for peripheral with name \(peripheral.name ?? "'unknown'"), with error: \(error), will try again"
             )
 
         } else {
@@ -260,7 +260,7 @@ class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDele
         heartbeat()
 
         if let error = error {
-            debug(.deviceManager, "    error: \(error.localizedDescription)")
+            debug(.deviceManager, "    error: \(error)")
         }
 
         // if self.peripheral == nil, then a manual disconnect or something like that has occured, no need to reconnect
@@ -279,7 +279,7 @@ class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDele
         debug(.deviceManager, "didDiscoverServices for peripheral with name \(peripheral.name ?? "'unknown'")")
 
         if let error = error {
-            debug(.deviceManager, "    didDiscoverServices error: \(error.localizedDescription)")
+            debug(.deviceManager, "    didDiscoverServices error: \(error)")
         }
 
         if let services = peripheral.services {
@@ -302,7 +302,7 @@ class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDele
         )
 
         if let error = error {
-            debug(.deviceManager, "    didDiscoverCharacteristicsFor error: \(error.localizedDescription)")
+            debug(.deviceManager, "    didDiscoverCharacteristicsFor error: \(error)")
         }
 
         if let characteristics = service.characteristics {
@@ -327,7 +327,7 @@ class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDele
         if let error = error {
             debug(
                 .deviceManager,
-                "didUpdateNotificationStateFor for peripheral with name \(peripheral.name ?? "'unkonwn'"), characteristic \(String(describing: characteristic.uuid)), error =  \(error.localizedDescription)"
+                "didUpdateNotificationStateFor for peripheral with name \(peripheral.name ?? "'unkonwn'"), characteristic \(String(describing: characteristic.uuid)), error =  \(error)"
             )
         }
     }

+ 3 - 3
Trio/Sources/APS/DeviceDataManager.swift

@@ -176,7 +176,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                             try self.privateContext.save()
 
                         } catch {
-                            print("Failed to delete OpenAPS_Battery entries: \(error.localizedDescription)")
+                            debug(.deviceManager, "Failed to delete OpenAPS_Battery entries: \(error)")
                         }
                     }
                 }
@@ -499,7 +499,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
     func pumpManager(_: PumpManager, didError error: PumpManagerError) {
         dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, "error: \(error.localizedDescription), reason: \(String(describing: error.failureReason))")
+        debug(.deviceManager, "error: \(error), reason: \(String(describing: error.failureReason))")
         errorSubject.send(error)
     }
 
@@ -675,7 +675,7 @@ extension BaseDeviceDataManager: AlertObserver {
             self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in
                 if let error = error {
                     self.alertHistoryStorage.ackAlert(alertIssueDate, error.localizedDescription)
-                    debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)")
+                    debug(.deviceManager, "acknowledge not succeeded with error \(error)")
                 } else {
                     self.alertHistoryStorage.ackAlert(alertIssueDate, nil)
                 }

+ 7 - 0
Trio/Sources/APS/Extensions/DecimalExtensions.swift

@@ -0,0 +1,7 @@
+import Foundation
+
+extension Decimal {
+    func clamp(to pickerSetting: PickerSetting) -> Decimal {
+        max(min(self, pickerSetting.max), pickerSetting.min)
+    }
+}

+ 4 - 4
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -11,7 +11,7 @@ protocol FetchGlucoseManager: SourceInfoProvider {
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource() async
     func removeCalibrations()
-    var glucoseSource: GlucoseSource! { get }
+    var glucoseSource: GlucoseSource? { get }
     var cgmManager: CGMManagerUI? { get }
     var cgmGlucoseSourceType: CGMType { get set }
     var cgmGlucosePluginId: String { get }
@@ -102,7 +102,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                                 glucose: newGlucose
                             )
                         } catch {
-                            debug(.deviceManager, "Failed to store glucose: \(error.localizedDescription)")
+                            debug(.deviceManager, "Failed to store glucose: \(error)")
                         }
                     }
                 }
@@ -113,7 +113,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         timer.resume()
     }
 
-    var glucoseSource: GlucoseSource!
+    var glucoseSource: GlucoseSource?
 
     func removeCalibrations() {
         calibrationService.removeAllCalibrations()
@@ -286,7 +286,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
 
     func sourceInfo() -> [String: Any]? {
-        glucoseSource.sourceInfo()
+        glucoseSource?.sourceInfo()
     }
 
     private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {

+ 1 - 1
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -369,7 +369,7 @@ final class OpenAPS {
             oref2_variables: oref2_variables
         )
 
-        debug(.openAPS, "OREF DETERMINATION: \(orefDetermination)")
+        debug(.openAPS, "\(simulation ? "[SIMULATION]" : "") OREF DETERMINATION: \(orefDetermination)")
 
         if var determination = Determination(from: orefDetermination), let deliverAt = determination.deliverAt {
             // set both timestamp and deliverAt to the SAME date; this will be updated for timestamp once it is enacted

+ 1 - 1
Trio/Sources/APS/OpenAPS/Script.swift

@@ -10,7 +10,7 @@ struct Script {
             do {
                 body = try String(contentsOf: url)
             } catch {
-                print("Error loading script: \(error.localizedDescription)")
+                debug(.openAPS, "Error loading script: \(error)")
                 body = "Error loading script"
             }
         } else {

+ 19 - 11
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -27,6 +27,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
     private let updateSubject = PassthroughSubject<Void, Never>()
 
+    private let settingsProvider = PickerSettingsProvider.shared
+
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
     }
@@ -111,7 +113,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
      - Returns: The computed duration in hours.
      */
-    private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
+    private func calculateComputedDuration(fpus: Decimal, timeCap: Decimal) -> Decimal {
         switch fpus {
         case ..<2:
             return 3
@@ -145,22 +147,25 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         createdAt: Date,
         actualDate: Date?
     ) -> ([CarbsEntry], Decimal) {
-        let interval = settings.settings.minuteInterval
-        let timeCap = settings.settings.timeCap
-        let adjustment = settings.settings.individualAdjustmentFactor
-        let delay = settings.settings.delay
+        let trioSettings = settings.settings
+        let providerSettings = settingsProvider.settings
+
+        let interval = trioSettings.minuteInterval.clamp(to: providerSettings.minuteInterval)
+        let timeCap = trioSettings.timeCap.clamp(to: providerSettings.timeCap)
+        let adjustment = trioSettings.individualAdjustmentFactor.clamp(to: providerSettings.individualAdjustmentFactor)
+        let delay = trioSettings.delay.clamp(to: providerSettings.delay)
 
         let kcal = protein * 4 + fat * 9
         let carbEquivalents = (kcal / 10) * adjustment
         let fpus = carbEquivalents / 10
         var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
 
-        var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
-        carbEquivalentSize /= Decimal(60 / interval)
+        var carbEquivalentSize: Decimal = carbEquivalents / computedDuration
+        carbEquivalentSize /= Decimal(60) / interval
 
         if carbEquivalentSize < 1.0 {
             carbEquivalentSize = 1.0
-            computedDuration = Int(carbEquivalents / carbEquivalentSize)
+            computedDuration = min(carbEquivalents / carbEquivalentSize, timeCap)
         }
 
         let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
@@ -172,9 +177,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
 
+        // convert Decimal minutes to TimeInterval in seconds
+        let delayTimeInterval = TimeInterval(delay * 60)
+        let intervalTimeInterval = TimeInterval(interval * 60)
         while carbEquivalents > 0, numberOfEquivalents > 0 {
-            useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
-                .addingTimeInterval(interval.minutes.timeInterval)
+            useDate = firstIndex ? useDate.addingTimeInterval(delayTimeInterval) : useDate
+                .addingTimeInterval(intervalTimeInterval)
             firstIndex = false
 
             let eachCarbEntry = CarbsEntry(
@@ -334,7 +342,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 }
 
             } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error)")
             }
         }
     }

+ 1 - 1
Trio/Sources/APS/Storage/ContactImageStorage.swift

@@ -61,7 +61,7 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
                 }
             }
         } catch {
-            debug(.default, "\(DebuggingIdentifiers.failed) Error fetching contact image entries: \(error.localizedDescription)")
+            debug(.default, "\(DebuggingIdentifiers.failed) Error fetching contact image entries: \(error)")
             return []
         }
     }

+ 4 - 4
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -55,7 +55,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                 return forecasts.map(\.objectID) as [NSManagedObjectID]
             } catch {
                 debugPrint(
-                    "Failed \(DebuggingIdentifiers.failed) to fetch Forecast IDs for OrefDetermination with ID \(determinationID): \(error.localizedDescription)"
+                    "Failed \(DebuggingIdentifiers.failed) to fetch Forecast IDs for OrefDetermination with ID \(determinationID): \(error)"
                 )
                 return []
             }
@@ -74,7 +74,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                 return forecastValues.map(\.objectID)
             } catch {
                 debugPrint(
-                    "Failed \(DebuggingIdentifiers.failed) to fetch Forecast Value IDs with ID \(forecastID): \(error.localizedDescription)"
+                    "Failed \(DebuggingIdentifiers.failed) to fetch Forecast Value IDs with ID \(forecastID): \(error)"
                 )
                 return []
             }
@@ -102,7 +102,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                 }
             } catch {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error)"
                 )
             }
             return (data.id, forecast, forecastValues)
@@ -196,7 +196,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                 }
             } catch {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch managed object with error: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch managed object with error: \(error)"
                 )
             }
 

+ 3 - 3
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -268,7 +268,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     fetchedDate = date
                 }
             } catch {
-                debugPrint("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription)")
+                debugPrint("Fetch error: \(DebuggingIdentifiers.failed) \(error)")
             }
         }
 
@@ -287,7 +287,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 let results = try self.context.fetch(fr)
                 date = results.first?.date
             } catch let error as NSError {
-                print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
+                debug(.storage, "Fetch error: \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
             }
         }
 
@@ -568,7 +568,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 debugPrint("\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
             } catch {
                 debugPrint(
-                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error.localizedDescription)"
+                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data: \(error)"
                 )
             }
         }

+ 1 - 1
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -232,7 +232,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                     "OverrideStorage: \(#function) \(DebuggingIdentifiers.succeeded) deleted override from core data"
                 )
             } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) Error deleting override: \(error.localizedDescription)")
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting override: \(error)")
             }
         }
     }

+ 1 - 1
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -158,7 +158,7 @@ final class BaseTDDStorage: TDDStorage, Injectable {
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
             } catch {
-                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to save TDD: \(error.localizedDescription)")
+                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to save TDD: \(error)")
             }
         }
     }

+ 5 - 4
Trio/Sources/Application/AppDelegate.swift

@@ -18,6 +18,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         // the next app boot, but this is fine since the app will need
         // to boot after a crash
         Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(crashReportingEnabled)
+        Crashlytics.crashlytics().setCustomValue(Bundle.main.appDevVersion ?? "unknown", forKey: "app_dev_version")
 
         return true
     }
@@ -40,13 +41,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
                 } catch {
                     debug(
                         .default,
-                        "\(DebuggingIdentifiers.failed) failed to handle remote notification with error: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) failed to handle remote notification with error: \(error)"
                     )
                     completionHandler(.failed)
                 }
             }
         } catch {
-            debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
+            debug(.remoteControl, "Error decoding push message: \(error)")
             completionHandler(.failed)
         }
     }
@@ -64,7 +65,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
             } catch {
                 debug(
                     .remoteControl,
-                    "\(DebuggingIdentifiers.failed) failed to register for remote notifications: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) failed to register for remote notifications: \(error)"
                 )
             }
         }
@@ -74,6 +75,6 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         _: UIApplication,
         didFailToRegisterForRemoteNotificationsWithError error: Error
     ) {
-        debug(.remoteControl, "Failed to register for remote notifications: \(error.localizedDescription)")
+        debug(.remoteControl, "Failed to register for remote notifications: \(error)")
     }
 }

+ 22 - 2
Trio/Sources/Application/TrioApp.swift

@@ -115,9 +115,23 @@ extension Notification.Name {
             "\(key): \(value.branch) \(value.commitSHA)"
         }.joined(separator: ", ")
 
+        /// The current development version of the app.
+        ///
+        /// Follows a semantic pattern where release versions are like `0.5.0`, and
+        /// development versions increment with a fourth component (e.g., `0.5.0.1`, `0.5.0.2`)
+        /// after the base release. For example:
+        /// - After release `0.5.0` → `0.5.0`
+        /// - First dev push → `0.5.0.1`
+        /// - Next dev push → `0.5.0.2`
+        /// - Next release `0.6.0` → `0.6.0`
+        /// - Next dev push → `0.6.0.1`
+        ///
+        /// If the dev version is unavailable, `"unknown"` is returned.
+        let devVersion = Bundle.main.appDevVersion ?? "unknown"
+
         debug(
             .default,
-            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [Branch: \(BuildDetails.shared.branchAndSha)] [submodules: \(submodulesInfo)]"
+            "Trio Started: v\(devVersion)(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [Branch: \(BuildDetails.shared.branchAndSha)] [submodules: \(submodulesInfo)]"
         )
         // Fix bug in iOS 18 related to the translucent tab bar
         configureTabBarAppearance()
@@ -162,7 +176,7 @@ extension Notification.Name {
             } catch {
                 debug(
                     .coreData,
-                    "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error)"
                 )
 
                 await MainActor.run {
@@ -447,3 +461,9 @@ extension Notification.Name {
         }
     }
 }
+
+public extension Bundle {
+    var appDevVersion: String? {
+        object(forInfoDictionaryKey: "AppDevVersion") as? String
+    }
+}

+ 2 - 2
Trio/Sources/Helpers/MainChartHelper.swift

@@ -70,7 +70,7 @@ enum MainChartHelper {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate duration for object with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate duration for object with error: \(error)"
             )
         }
 
@@ -85,7 +85,7 @@ enum MainChartHelper {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate target for object with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate target for object with error: \(error)"
             )
         }
         return nil

+ 3 - 3
Trio/Sources/Helpers/PropertyWrappers/PersistedProperty.swift

@@ -81,7 +81,7 @@ import Foundation
                 }
                 return value
             } catch {
-                debug(.storage, "❌ [PersistedProperty:\(key)] Failed to read value: \(error.localizedDescription)")
+                debug(.storage, "❌ [PersistedProperty:\(key)] Failed to read value: \(error)")
                 return nil
             }
         }
@@ -91,7 +91,7 @@ import Foundation
                     try FileManager.default.removeItem(at: storageURL)
                     debug(.storage, "[PersistedProperty:\(key)] Removed value.")
                 } catch {
-                    debug(.storage, "❌ [PersistedProperty:\(key)] Failed to remove value: \(error.localizedDescription)")
+                    debug(.storage, "❌ [PersistedProperty:\(key)] Failed to remove value: \(error)")
                 }
                 return
             }
@@ -108,7 +108,7 @@ import Foundation
 
                 debug(.storage, "✅ [PersistedProperty:\(key)] Saved value successfully.")
             } catch {
-                debug(.storage, "❌ [PersistedProperty:\(key)] Failed to write value: \(error.localizedDescription)")
+                debug(.storage, "❌ [PersistedProperty:\(key)] Failed to write value: \(error)")
             }
         }
     }

+ 42 - 0
Trio/Sources/Helpers/TimeAgoFormatter.swift

@@ -0,0 +1,42 @@
+//
+//  TimeAgoFormatter.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 21.05.25.
+//
+import Foundation
+
+enum TimeAgoFormatter {
+    /// Returns a user-facing string for how many minutes ago the given date occurred,
+    /// formatted with non-breaking spaces and localized abbreviation.
+    ///
+    /// - Parameter date: The past `Date` to calculate elapsed time from.
+    /// - Returns: A formatted string like `"< 1 m"` or `"2 m"`. Returns `"--"` if the date is `nil`.
+    static func minutesAgo(from date: Date?) -> String {
+        guard let date = date else {
+            return "--"
+        }
+
+        let secondsAgo = -date.timeIntervalSinceNow
+        let minutesAgo = Int(floor(secondsAgo / 60))
+
+        if minutesAgo >= 1 {
+            let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? "\(minutesAgo)"
+            return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+        } else {
+            return "<" + "\u{00A0}" + "1" + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+        }
+    }
+
+    // Calculates the floored integer value of how many full minutes ago the given date occurred.
+    ///
+    /// - Parameter date: The past `Date` to compare against the current time.
+    /// - Returns: An integer representing the number of full minutes since the given date.
+    ///            Returns `Int.max` if the date is `nil`.
+    static func minutesAgoValue(from date: Date?) -> Int {
+        guard let date = date else {
+            return Int.max
+        }
+        return Int(floor(-date.timeIntervalSinceNow / 60))
+    }
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 24410 - 20554
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 35 - 1
Trio/Sources/Logger/Logger.swift

@@ -40,6 +40,29 @@ func info(
     }.perform()
 }
 
+func info(
+    _ category: Logger.Category,
+    _ message: String,
+    notificationText: String,
+    type: MessageType = .info,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) {
+    DispatchWorkItem(qos: .background, flags: .enforceQoS) {
+        loggerLock.perform {
+            category.logger.info(
+                message,
+                notificationText: notificationText,
+                type: type,
+                file: file,
+                function: function,
+                line: line
+            )
+        }
+    }.perform()
+}
+
 func warning(
     _ category: Logger.Category,
     _ message: String,
@@ -246,11 +269,22 @@ final class Logger {
         function: String = #function,
         line: UInt = #line
     ) {
+        info(message, notificationText: message, type: type, file: file, function: function, line: line)
+    }
+
+    func info(
+        _ message: String,
+        notificationText: String,
+        type: MessageType = .info,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) {
         let printedMessage = "INFO: \(message)"
         os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, printedMessage)
         reporter.log(category.name, printedMessage, file: file, function: function, line: line)
 
-        showAlert(message, type: type)
+        showAlert(notificationText, type: type)
     }
 
     func warning(

+ 1 - 1
Trio/Sources/Models/DecimalPickerSettings.swift

@@ -131,7 +131,7 @@ struct DecimalPickerSettings {
     )
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var delay = PickerSetting(value: 60, step: 10, min: 60, max: 120, type: PickerSetting.PickerSettingType.minute)
+    var delay = PickerSetting(value: 60, step: 5, min: 15, max: 120, type: PickerSetting.PickerSettingType.minute)
     var minuteInterval = PickerSetting(value: 30, step: 5, min: 10, max: 60, type: PickerSetting.PickerSettingType.minute)
     var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)

+ 6 - 11
Trio/Sources/Models/TrioSettings.swift

@@ -42,15 +42,14 @@ struct TrioSettings: JSON, Equatable {
     var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
     var individualAdjustmentFactor: Decimal = 0.5
-    var timeCap: Int = 8
-    var minuteInterval: Int = 30
-    var delay: Int = 60
+    var timeCap: Decimal = 8
+    var minuteInterval: Decimal = 30
+    var delay: Decimal = 60
     var useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
     var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var low: Decimal = 70
-    var hours: Int = 6
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
@@ -168,15 +167,15 @@ extension TrioSettings: Decodable {
             settings.overrideFactor = overrideFactor
         }
 
-        if let timeCap = try? container.decode(Int.self, forKey: .timeCap) {
+        if let timeCap = try? container.decode(Decimal.self, forKey: .timeCap) {
             settings.timeCap = timeCap
         }
 
-        if let minuteInterval = try? container.decode(Int.self, forKey: .minuteInterval) {
+        if let minuteInterval = try? container.decode(Decimal.self, forKey: .minuteInterval) {
             settings.minuteInterval = minuteInterval
         }
 
-        if let delay = try? container.decode(Int.self, forKey: .delay) {
+        if let delay = try? container.decode(Decimal.self, forKey: .delay) {
             settings.delay = delay
         }
 
@@ -238,10 +237,6 @@ extension TrioSettings: Decodable {
             settings.high = high
         }
 
-        if let hours = try? container.decode(Int.self, forKey: .hours) {
-            settings.hours = hours
-        }
-
         if let glucoseColorScheme = try? container.decode(GlucoseColorScheme.self, forKey: .glucoseColorScheme) {
             settings.glucoseColorScheme = glucoseColorScheme
         }

+ 9 - 9
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -77,7 +77,7 @@ extension Adjustments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error)"
             )
         }
     }
@@ -124,7 +124,7 @@ extension Adjustments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to save custom override: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to save custom override: \(error)"
             )
         }
     }
@@ -166,7 +166,7 @@ extension Adjustments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to save override preset: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to save override preset: \(error)"
             )
         }
     }
@@ -182,7 +182,7 @@ extension Adjustments.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Failed to setup override presets: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Failed to setup override presets: \(error)"
                 )
             }
         }
@@ -197,7 +197,7 @@ extension Adjustments.StateModel {
             overridePresets = overrideObjects
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides: \(error)"
             )
         }
     }
@@ -211,7 +211,7 @@ extension Adjustments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to delete override preset: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to delete override preset: \(error)"
             )
         }
     }
@@ -238,7 +238,7 @@ extension Adjustments.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Failed to update override configuration: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Failed to update override configuration: \(error)"
                 )
             }
         }
@@ -274,7 +274,7 @@ extension Adjustments.StateModel {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active Override: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active Override: \(error)"
             )
         }
     }
@@ -298,7 +298,7 @@ extension Adjustments.StateModel {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous Override: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous Override: \(error)"
             )
         }
     }

+ 6 - 6
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -22,7 +22,7 @@ extension Adjustments.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to load latest temp target configuration with error: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to load latest temp target configuration with error: \(error)"
                 )
             }
         }
@@ -59,7 +59,7 @@ extension Adjustments.StateModel {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error)"
             )
         }
     }
@@ -79,7 +79,7 @@ extension Adjustments.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Failed to setup temp targets: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Failed to setup temp targets: \(error)"
                 )
             }
         }
@@ -186,7 +186,7 @@ extension Adjustments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to enable scheduled temp target: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to enable scheduled temp target: \(error)"
             )
         }
     }
@@ -329,7 +329,7 @@ extension Adjustments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error)"
             )
         }
     }
@@ -360,7 +360,7 @@ extension Adjustments.StateModel {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error)"
             )
         }
     }

+ 1 - 1
Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -80,7 +80,7 @@ extension AlgorithmAdvancedSettings {
                             } catch {
                                 debug(
                                     .default,
-                                    "\(DebuggingIdentifiers.failed) failed to upload DIA to Nightscout: \(error.localizedDescription)"
+                                    "\(DebuggingIdentifiers.failed) failed to upload DIA to Nightscout: \(error)"
                                 )
                             }
                         }

+ 6 - 6
Trio/Sources/Modules/AppDiagnostics/View/AppDiagnosticsRootView.swift

@@ -9,8 +9,7 @@ extension AppDiagnostics {
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
-
-        @State private var shouldDisplayPrivacyPolicy: Bool = false
+        @Environment(\.openURL) var openURL
 
         var body: some View {
             List {
@@ -90,13 +89,14 @@ extension AppDiagnostics {
             .toolbar {
                 ToolbarItem(placement: .topBarTrailing) {
                     Button("Privacy Policy") {
-                        shouldDisplayPrivacyPolicy = true
+                        if let url = URL(string: "https://github.com/nightscout/Trio/blob/dev/PRIVACY_POLICY.md") {
+                            openURL(url)
+                        } else {
+                            debug(.default, "Invalid URL! Could not gracefully unwrap privacy policy link!")
+                        }
                     }
                 }
             }
-            .sheet(isPresented: $shouldDisplayPrivacyPolicy) {
-                PrivacyPolicyView()
-            }
         }
     }
 }

+ 0 - 233
Trio/Sources/Modules/AppDiagnostics/View/PrivacyPolicyView.swift

@@ -1,233 +0,0 @@
-//
-//  PrivacyPolicyView.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 17.04.25.
-//
-import SwiftUI
-
-struct PrivacyPolicyView: View {
-    @Environment(\.openURL) var openURL
-    @Environment(\.dismiss) var dismiss
-
-    var body: some View {
-        NavigationStack {
-            List {
-                VStack(alignment: .leading, spacing: 20) {
-                    Text("Introduction").font(.headline).bold().foregroundStyle(Color.primary)
-                    Text(
-                        "This Privacy Policy explains how we collect, use, and share information when you use Trio. We respect your privacy and are committed to protecting your personal data. Please read this Privacy Policy carefully to understand our practices regarding your personal data."
-                    )
-
-                    Divider()
-
-                    Text("Information We Collect").font(.headline).bold().foregroundStyle(Color.primary)
-                    Text("What We Do NOT Collect").foregroundStyle(Color.primary)
-
-                    Text("For complete transparency, we want to clarify that Trio does not collect:")
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        BulletPoint(String(localized: "Blood glucose (BG) readings"))
-                        BulletPoint(String(localized: "Treatment data"))
-                        BulletPoint(String(localized: "Total daily doses (TDD)"))
-                        BulletPoint(String(localized: "Any health-related statistics or personal medical information"))
-                        BulletPoint(String(localized: "Personal identifiable information such as name, address, or email"))
-                    }
-
-                    Text("Crash Reporting (Opt-In by default, with ability to Opt-Out)").foregroundStyle(Color.primary)
-                    Text(
-                        "Trio uses Google Firebase Crashlytics to collect crash reports. During the initial app setup (onboarding process), you will be asked to opt in to crash reporting. The onboarding process is the series of screens you see when first launching Trio that helps you set up the app."
-                    )
-
-                    Text("The following information may be sent to Crashlytics when Trio crashes:")
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        BulletPoint(
-                            String(
-                                localized: "Time and date of the crash (example: \"Trio crashed on April 6, 2025 at 2:15 PM\")"
-                            )
-                        )
-                        BulletPoint(
-                            String(
-                                localized: "Device state at the time of the crash (example: \"Trio was in the foreground\" or \"Battery level was 42%\")"
-                            )
-                        )
-                        BulletPoint(
-                            String(localized: "Stack trace information (technical information showing which line of code failed)")
-                        )
-                        BulletPoint(
-                            String(localized: "Device model and OS version (example: \"iPhone 14 Pro running iOS 17.4.1\")")
-                        )
-                        BulletPoint(
-                            String(
-                                localized: "A generated unique identifier (a random code like \"A7B2C9D3\" that doesn't identify you personally)"
-                            )
-                        )
-                    }
-
-                    Text("Debug Symbols (dSYMs)").foregroundStyle(Color.primary)
-
-                    Text(
-                        "When we build the Trio app, we create special files called debug symbols (dSYMs) that help us read crash reports. Think of these like a decoder ring for crashes:"
-                    )
-
-                    Text(
-                        "Without dSYMs, a crash might look like: \"Error at memory address 0x1234ABCD\". With dSYMs, we can see: \"Error in function 'calculateInsulin' at line 157\""
-                    )
-
-                    Text(
-                        "These files only contain code-related information that helps us understand where crashes happen. They contain no personal information about you or how you use Trio."
-                    )
-
-                    Divider()
-
-                    Text("How We Use Your Information").font(.headline).bold().foregroundStyle(Color.primary)
-
-                    Text("We use anonymous crash report information exclusively to:")
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        BulletPoint(String(localized: "Identify and fix bugs and crashes"))
-                        BulletPoint(String(localized: "Improve Trio's stability"))
-                    }
-
-                    Text("We do not use this information for any other purpose, such as analytics, marketing, or user profiling.")
-
-                    Divider()
-
-                    Text("Data Sharing and Third-Party Services").font(.headline).bold().foregroundStyle(Color.primary)
-
-                    Text("Crashlytics").foregroundStyle(Color.primary)
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text(
-                            "We use Google Firebase Crashlytics to collect and analyze crash reports. Crashlytics' privacy practices are governed by the Google Privacy Policy. For more information about how Crashlytics processes data, please visit their documentation."
-                        )
-
-                        Button {
-                            openURL(URL(string: "https://policies.google.com/privacy")!)
-                        } label: {
-                            Text("Google Privacy Policy")
-                                .padding(.horizontal, 12)
-                                .padding(.vertical, 8)
-                                .background(Color.blue.opacity(0.2))
-                                .cornerRadius(8)
-                        }
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .padding(.horizontal)
-                    }
-
-                    Text("Open Source Contributors").foregroundStyle(Color.primary)
-
-                    Text(
-                        "As an open source project, crash reports and debugging information may be visible to project contributors who help maintain and improve Trio. All contributors are expected to adhere to this privacy policy and handle any data responsibly."
-                    )
-
-                    Divider()
-
-                    Text("Opting Out and Data Retention")
-
-                    Text("You can opt out of crash reporting at any time through the Trio settings. If you opt out:")
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        BulletPoint(String(localized: "No new crash data will be collected or sent to us"))
-                        BulletPoint(
-                            String(localized: "Previously collected crash data will still be retained for approximately 90 days")
-                        )
-                    }
-
-                    Text(
-                        "To avoid sending dSYMs to Crashlytics, you can delete the Trio target Build Phase script, titled \"Copy dSYMs to Crashlytics\"."
-                    )
-
-                    Divider()
-
-                    Text("Your Rights").font(.headline).bold().foregroundStyle(Color.primary)
-
-                    Text("You have certain rights regarding your information, including:")
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        BulletPoint(String(localized: "The right to opt-out of crash reporting"))
-                        BulletPoint(String(localized: "The right to request deletion of your data"))
-                    }
-
-                    Text(
-                        "To opt-out of crash reporting, please see the section above for details about how to configure Trio to not record crash reports."
-                    )
-
-                    Text(
-                        "The information we store is anonymous, so we are unable to look up information for a particular individual. However, our general data retention policy ensures that data older than 90 days is deleted, enabling us to accommodate data deletion requests by design despite having anonymous data."
-                    )
-
-                    Divider()
-
-                    Text("Changes to This Privacy Policy").font(.headline).bold().foregroundStyle(Color.primary)
-
-                    Text(
-                        "We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the \"Last Updated\" date."
-                    )
-
-                    Divider()
-
-                    Text("Contact Us").font(.headline).bold().foregroundStyle(Color.primary)
-
-                    VStack(alignment: .leading, spacing: 10) {
-                        Text(
-                            "If you have any questions about this Privacy Policy, please contact us on Discord, or send us an email."
-                        ).multilineTextAlignment(.leading)
-
-                        HStack(alignment: .center, spacing: 10) {
-                            Button {
-                                openURL(URL(string: "http://discord.diy-trio.org/")!)
-                            } label: {
-                                Text("Trio Discord")
-                                    .padding(.horizontal, 12)
-                                    .padding(.vertical, 8)
-                                    .background(Color.blue.opacity(0.2))
-                                    .cornerRadius(8)
-                            }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .padding(.horizontal)
-
-                            Button {
-                                openURL(URL(string: "mailto:trio.diy.diabetes@gmail.com")!)
-                            } label: {
-                                Text("Email us")
-                                    .padding(.horizontal, 12)
-                                    .padding(.vertical, 8)
-                                    .background(Color.blue.opacity(0.2))
-                                    .cornerRadius(8)
-                            }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .padding(.horizontal)
-                        }
-                    }
-
-                    Divider()
-
-                    HStack {
-                        Text("Last Updated:").bold()
-                        Text("April 15, 2025")
-                    }
-                    .font(.headline).foregroundStyle(Color.primary)
-                }
-                .font(.footnote)
-                .foregroundStyle(Color.secondary)
-                .listRowBackground(Color.clear)
-                .fixedSize(horizontal: false, vertical: true)
-                .multilineTextAlignment(.leading)
-            }
-            .scrollContentBackground(.hidden)
-            .navigationBarTitle("Privacy Policy", displayMode: .inline)
-
-            Spacer()
-
-            Button {
-                dismiss()
-            } label: {
-                Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
-            }
-            .buttonStyle(.bordered)
-            .padding([.top, .horizontal])
-        }.ignoresSafeArea(edges: .top)
-    }
-}

+ 2 - 2
Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -49,7 +49,7 @@ extension AutosensSettings {
                 } catch {
                     debug(
                         .default,
-                        "\(DebuggingIdentifiers.failed) Error fetching determination IDs: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) Error fetching determination IDs: \(error)"
                     )
                 }
             }
@@ -64,7 +64,7 @@ extension AutosensSettings {
 
             } catch {
                 debugPrint(
-                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
+                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error)"
                 )
             }
         }

+ 1 - 1
Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -98,7 +98,7 @@ extension BasalProfileEditor {
                                 debug(.nightscout, "Attempting to upload basal rates to Nightscout")
                                 try await self.nightscout.uploadProfiles()
                             } catch {
-                                debug(.default, "Failed to upload basal rates to Nightscout: \(error.localizedDescription)")
+                                debug(.default, "Failed to upload basal rates to Nightscout: \(error)")
                             }
                         }
                     case .failure:

+ 1 - 1
Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -81,7 +81,7 @@ extension Calibrations {
                     return
                 }
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) Failed to add calibration: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to add calibration: \(error)")
             }
         }
 

+ 1 - 1
Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift

@@ -74,7 +74,7 @@ extension CarbRatioEditor {
                     debug(.nightscout, "Attempting to upload CRs to Nightscout")
                     try await self.nightscout.uploadProfiles()
                 } catch {
-                    debug(.default, "Failed to upload CRs to Nightscout: \(error.localizedDescription)")
+                    debug(.default, "Failed to upload CRs to Nightscout: \(error)")
                 }
             }
         }

+ 3 - 1
Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift

@@ -189,7 +189,9 @@ struct AddContactImageSheet: View {
             Button(action: {
                 saveNewEntry()
             }, label: {
-                Text("Save").padding(10)
+                Text("Save")
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding(10)
             })
                 .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
                 .background(Color(.systemBlue))

+ 3 - 1
Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift

@@ -164,7 +164,9 @@ struct ContactImageDetailView: View {
             Button(action: {
                 saveChanges()
             }, label: {
-                Text("Save").padding(10)
+                Text("Save")
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding(10)
             })
                 .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
                 .background(isUnchanged ? Color(.systemGray4) : Color(.systemBlue))

+ 7 - 7
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -87,7 +87,7 @@ extension DataTable {
                     )
                 } catch {
                     debugPrint(
-                        "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
+                        "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error)"
                     )
                 }
             }
@@ -116,7 +116,7 @@ extension DataTable {
                     try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
 
                 } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error.localizedDescription)")
+                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error)")
                     await MainActor.run {
                         carbEntryDeleted = false
                         waitForSuggestion = false
@@ -210,7 +210,7 @@ extension DataTable {
                         )
                     }
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error.localizedDescription)")
+                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error)")
                 }
             }
         }
@@ -256,7 +256,7 @@ extension DataTable {
                 try await apsManager.determineBasalSync()
             } catch {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error)"
                 )
                 await MainActor.run {
                     insulinEntryDeleted = false
@@ -291,7 +291,7 @@ extension DataTable {
 
                     debug(.default, "Successfully deleted the treatment object.")
                 } catch {
-                    debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
+                    debug(.default, "Failed to delete the treatment object: \(error)")
                 }
             }
         }
@@ -343,7 +343,7 @@ extension DataTable {
                     try await apsManager.determineBasalSync()
 
                 } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error.localizedDescription)")
+                    debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error)")
                 }
             }
         }
@@ -470,7 +470,7 @@ extension DataTable {
                         date: entryDate
                     )
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error.localizedDescription)")
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error)")
                     return nil
                 }
             }

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift

@@ -12,7 +12,7 @@ extension Home.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up battery array: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up battery array: \(error)"
                 )
             }
         }

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/CurrentTDDSetup.swift

@@ -11,7 +11,7 @@ extension Home.StateModel {
                 // Get the NSManagedObjects and map them to TDD on the Main Thread
                 try await updateTDDArray(with: tddObjectIds, keyPath: \.fetchedTDDs)
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch TDDs: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch TDDs: \(error)")
             }
         }
     }

+ 2 - 2
Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift

@@ -19,9 +19,9 @@ extension Home.StateModel {
 
                 await updateForecastData()
             } catch let error as CoreDataError {
-                debug(.default, "Core Data error in setupDeterminationsArray: \(error.localizedDescription)")
+                debug(.default, "Core Data error in setupDeterminationsArray: \(error)")
             } catch {
-                debug(.default, "Unexpected error in setupDeterminationsArray: \(error.localizedDescription)")
+                debug(.default, "Unexpected error in setupDeterminationsArray: \(error)")
             }
         }
     }

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -23,7 +23,7 @@ extension Home.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to preprocess forecast data: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to preprocess forecast data: \(error)"
             )
             return []
         }

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -12,7 +12,7 @@ extension Home.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error)"
                 )
             }
         }

+ 4 - 4
Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift

@@ -11,9 +11,9 @@ extension Home.StateModel {
                     .getNSManagedObject(with: ids, context: viewContext)
                 await updateOverrideArray(with: overrideObjects)
             } catch let error as CoreDataError {
-                debug(.default, "Core Data error in setupOverrides: \(error.localizedDescription)")
+                debug(.default, "Core Data error in setupOverrides: \(error)")
             } catch {
-                debug(.default, "Unexpected error in setupOverrides: \(error.localizedDescription)")
+                debug(.default, "Unexpected error in setupOverrides: \(error)")
             }
         }
     }
@@ -55,9 +55,9 @@ extension Home.StateModel {
                     .getNSManagedObject(with: ids, context: viewContext)
                 await updateOverrideRunStoredArray(with: overrideRunObjects)
             } catch let error as CoreDataError {
-                debug(.default, "Core Data error in setupOverrideRunStored: \(error.localizedDescription)")
+                debug(.default, "Core Data error in setupOverrideRunStored: \(error)")
             } catch {
-                debug(.default, "Unexpected error in setupOverrideRunStored: \(error.localizedDescription)")
+                debug(.default, "Unexpected error in setupOverrideRunStored: \(error)")
             }
         }
     }

+ 3 - 3
Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -12,7 +12,7 @@ extension Home.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up insulin array: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up insulin array: \(error)"
                 )
             }
         }
@@ -62,7 +62,7 @@ extension Home.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error)"
                 )
             }
         }
@@ -92,7 +92,7 @@ extension Home.StateModel {
             lastPumpBolus = try viewContext.existingObject(with: ID) as? PumpEventStored
         } catch {
             debugPrint(
-                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error.localizedDescription)"
+                "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the insulin array: \(error)"
             )
         }
     }

+ 2 - 2
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -12,7 +12,7 @@ extension Home.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up tempTargetStored: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up tempTargetStored: \(error)"
                 )
             }
         }
@@ -50,7 +50,7 @@ extension Home.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up temp targetsRunStored: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up temp targetsRunStored: \(error)"
                 )
             }
         }

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -335,7 +335,7 @@ extension Home {
                 .map { [weak self] error in
                     self?.errorDate = error == nil ? nil : Date()
                     if let error = error {
-                        info(.default, error.localizedDescription)
+                        info(.default, String(describing: error), notificationText: error.localizedDescription)
                     }
                     return error?.localizedDescription
                 }

+ 1 - 11
Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -81,17 +81,7 @@ struct CurrentGlucoseView: View {
                         }
                     }
                     HStack {
-                        let minutesAgo = -1 * (glucose.last?.date?.timeIntervalSinceNow ?? 0) / 60
-                        var minutesAgoString: String {
-                            if minutesAgo > 1 {
-                                let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
-                                return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
-                            } else {
-                                return "<" + "\u{00A0}" + "1" + "\u{00A0}" +
-                                    String(localized: "m", comment: "Abbreviation for Minutes")
-                            }
-                        }
-
+                        let minutesAgoString = TimeAgoFormatter.minutesAgo(from: glucose.last?.date)
                         Group {
                             Text(minutesAgoString)
                             Text(delta)

+ 2 - 6
Trio/Sources/Modules/Home/View/Header/LoopView.swift

@@ -57,15 +57,11 @@ struct LoopView: View {
     }
 
     private var timeString: String {
-        let minutesAgo = -1 * lastLoopDate.timeIntervalSinceNow / 60
-        let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
-
+        let minutesAgo = TimeAgoFormatter.minutesAgoValue(from: lastLoopDate)
         if minutesAgo > 1440 {
             return "--"
-        } else if minutesAgo <= 1 {
-            return "<" + "\u{00A0}" + "1" + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         } else {
-            return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+            return TimeAgoFormatter.minutesAgo(from: lastLoopDate)
         }
     }
 

+ 18 - 1
Trio/Sources/Modules/Home/View/Header/PumpView.swift

@@ -16,6 +16,23 @@ struct PumpView: View {
         return formatter
     }
 
+    private var hourglassIcon: String {
+        guard let expiration = expiresAtDate else { return "hourglass" }
+
+        let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600
+
+        switch hoursRemaining {
+        case 60 ... 72:
+            return "hourglass.bottomhalf.filled"
+        case 12 ..< 60:
+            return "hourglass"
+        case -8 ..< 12:
+            return "hourglass.tophalf.filled"
+        default:
+            return "hourglass"
+        }
+    }
+
     var body: some View {
         if let pumpStatusHighlightMessage = pumpStatusHighlightMessage { // display message instead pump info
             VStack(alignment: .center) {
@@ -80,7 +97,7 @@ struct PumpView: View {
 
                 if let date = expiresAtDate {
                     HStack {
-                        Image(systemName: "stopwatch.fill")
+                        Image(systemName: hourglassIcon)
                             .font(.callout)
                             .foregroundStyle(timerColor)
 

+ 1 - 1
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -177,7 +177,7 @@ extension Home {
                 )
             }
 
-            return rateString + " " + String(localized: " U/hr", comment: "Unit per hour with space") + manualBasalString
+            return rateString + String(localized: " U/hr", comment: "Unit per hour with space") + manualBasalString
         }
 
         var overrideString: String? {

+ 1 - 1
Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -105,7 +105,7 @@ extension ISFEditor {
                 } catch {
                     debug(
                         .default,
-                        "\(DebuggingIdentifiers.failed) Faile to upload ISF to Nightscout: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) Faile to upload ISF to Nightscout: \(error)"
                     )
                 }
             }

+ 5 - 0
Trio/Sources/Modules/Main/MainDataFlow.swift

@@ -9,6 +9,11 @@ enum Main {
 
         var id: Int { screen.id }
     }
+
+    struct SecondaryModalWrapper: Identifiable {
+        let id = UUID()
+        let view: AnyView
+    }
 }
 
 protocol MainProvider: Provider {}

+ 10 - 16
Trio/Sources/Modules/Main/MainStateModel.swift

@@ -9,10 +9,8 @@ extension Main {
         @Injected() private var apsManager: APSManager!
         @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
         @Injected() var broadcaster: Broadcaster!
-        private(set) var modal: Modal?
-        @Published var isModalPresented = false
-        @Published var isSecondaryModalPresented = false
-        @Published var secondaryModalView: AnyView? = nil
+        @Published var modal: Modal?
+        @Published var secondaryModal: SecondaryModalWrapper?
 
         @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
         private var timers: [TimeInterval: Timer] = [:]
@@ -250,14 +248,11 @@ extension Main {
                 .map { $0?.modal(resolver: self.resolver!) }
                 .removeDuplicates { $0?.id == $1?.id }
                 .receive(on: DispatchQueue.main)
-                .sink { modal in
-                    self.modal = modal
-                    self.isModalPresented = modal != nil
-                }
-                .store(in: &lifetime)
+                .assign(to: &$modal)
 
-            $isModalPresented
-                .filter { !$0 }
+            $modal
+                .removeDuplicates { $0?.id == $1?.id }
+                .filter { $0 == nil }
                 .sink { _ in
                     self.router.mainModalScreen.send(nil)
                 }
@@ -277,14 +272,13 @@ extension Main {
             router.mainSecondaryModalView
                 .receive(on: DispatchQueue.main)
                 .sink { view in
-                    self.secondaryModalView = view
-                    self.isSecondaryModalPresented = view != nil
+                    self.secondaryModal = view.map { SecondaryModalWrapper(view: $0) }
                 }
                 .store(in: &lifetime)
 
-            $isSecondaryModalPresented
-                .removeDuplicates()
-                .filter { !$0 }
+            $secondaryModal
+                .removeDuplicates { $0?.id == $1?.id }
+                .filter { $0 == nil }
                 .sink { _ in
                     self.router.mainSecondaryModalView.send(nil)
                 }

+ 5 - 4
Trio/Sources/Modules/Main/View/MainRootView.swift

@@ -11,13 +11,14 @@ extension Main {
 
         var body: some View {
             router.view(for: .home)
-                .sheet(isPresented: $state.isModalPresented) {
-                    NavigationView { self.state.modal!.view }
+                .sheet(item: $state.modal) { modal in
+                    NavigationView { modal.view }
                         .navigationViewStyle(StackNavigationViewStyle())
                 }
-                .sheet(isPresented: $state.isSecondaryModalPresented) {
-                    state.secondaryModalView ?? EmptyView().asAny()
+                .sheet(item: $state.secondaryModal) { wrapper in
+                    wrapper.view
                 }
+
                 .onAppear(perform: configureView)
                 .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
         }

+ 13 - 24
Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift

@@ -16,36 +16,25 @@ extension MealSettings {
         override func subscribe() {
             units = settingsManager.settings.units
 
-            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             subscribeSetting(\.maxCarbs, on: $maxCarbs) { maxCarbs = $0 }
             subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
             subscribeSetting(\.maxProtein, on: $maxProtein) { maxProtein = $0 }
 
-            subscribeSetting(\.timeCap, on: $timeCap.map(Int.init), initial: {
-                timeCap = Decimal($0)
-            }, map: {
-                $0
-            })
-
             subscribePreferencesSetting(\.maxMealAbsorptionTime, on: $maxMealAbsorptionTime) { maxMealAbsorptionTime = $0 }
 
-            subscribeSetting(\.minuteInterval, on: $minuteInterval.map(Int.init), initial: {
-                minuteInterval = Decimal($0)
-            }, map: {
-                $0
-            })
-
-            subscribeSetting(\.delay, on: $delay.map(Int.init), initial: {
-                delay = Decimal($0)
-            }, map: {
-                $0
-            })
-
-            subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor, initial: {
-                individualAdjustmentFactor = $0
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
+
+            // "Fat and Protein Delay"
+            subscribeSetting(\.delay, on: $delay) { delay = $0 }
+
+            // "Maximum Duration"
+            subscribeSetting(\.timeCap, on: $timeCap) { timeCap = $0 }
+
+            // "Spread Interval"
+            subscribeSetting(\.minuteInterval, on: $minuteInterval) { minuteInterval = $0 }
+
+            // "Fat and Protein Percentage"
+            subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor) { individualAdjustmentFactor = $0 }
         }
     }
 }

+ 3 - 3
Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -63,7 +63,7 @@ extension NightscoutConfig {
                             } catch {
                                 debug(
                                     .default,
-                                    "\(DebuggingIdentifiers.failed) failed to upload profiles: \(error.localizedDescription)"
+                                    "\(DebuggingIdentifiers.failed) failed to upload profiles: \(error)"
                                 )
                             }
                         }
@@ -152,10 +152,10 @@ extension NightscoutConfig {
                         await self.healthKitManager.uploadGlucose()
                     }
                 } catch let error as CoreDataError {
-                    debug(.nightscout, "Core Data error while storing backfilled glucose: \(error.localizedDescription)")
+                    debug(.nightscout, "Core Data error while storing backfilled glucose: \(error)")
                     message = "Error: \(error.localizedDescription)"
                 } catch {
-                    debug(.nightscout, "Unexpected error while storing backfilled glucose: \(error.localizedDescription)")
+                    debug(.nightscout, "Unexpected error while storing backfilled glucose: \(error)")
                     message = "Error: \(error.localizedDescription)"
                 }
             } else {

+ 0 - 3
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -7,9 +7,6 @@ extension NightscoutConfig {
         let resolver: Resolver
         let displayClose: Bool
         @StateObject var state = StateModel()
-        @State var importAlert: Alert?
-        @State var isImportAlertPresented = false
-        @State var importedHasRun = false
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: AnyView?

+ 10 - 18
Trio/Sources/Modules/Onboarding/OnboardingStateModel+Nightscout.swift

@@ -59,13 +59,10 @@ extension Onboarding.StateModel {
 
         do {
             guard let fetchedProfile = await nightscoutManager.importSettings() else {
-                await MainActor.run {
-                    nightscoutImportStatus = .failed
-                }
                 throw NSError(
                     domain: "ImportError",
                     code: 1,
-                    userInfo: [NSLocalizedDescriptionKey: "Cannot find the default Nightscout Profile."]
+                    userInfo: [NSLocalizedDescriptionKey: "Cannot find the Nightscout Profile named \"default\"."]
                 )
             }
 
@@ -83,9 +80,6 @@ extension Onboarding.StateModel {
             }
 
             if carbratios.contains(where: { $0.ratio <= 0 }) {
-                await MainActor.run {
-                    nightscoutImportStatus = .failed
-                }
                 throw NSError(
                     domain: "ImportError",
                     code: 2,
@@ -105,9 +99,6 @@ extension Onboarding.StateModel {
             }
 
             if basals.contains(where: { $0.rate <= 0 }) {
-                await MainActor.run {
-                    nightscoutImportStatus = .failed
-                }
                 throw NSError(
                     domain: "ImportError",
                     code: 3,
@@ -116,9 +107,6 @@ extension Onboarding.StateModel {
             }
 
             if basals.reduce(0, { $0 + $1.rate }) <= 0 {
-                await MainActor.run {
-                    nightscoutImportStatus = .failed
-                }
                 throw NSError(
                     domain: "ImportError",
                     code: 4,
@@ -139,9 +127,6 @@ extension Onboarding.StateModel {
             }
 
             if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
-                await MainActor.run {
-                    nightscoutImportStatus = .failed
-                }
                 throw NSError(
                     domain: "ImportError",
                     code: 5,
@@ -178,8 +163,9 @@ extension Onboarding.StateModel {
             )
         } catch {
             await MainActor.run {
-                self.nightscoutImportErrors.append(error.localizedDescription)
-                debug(.service, "Settings import failed with error: \(error.localizedDescription)")
+                self.nightscoutImportError = NightscoutImportError(message: error.localizedDescription)
+                self.nightscoutImportStatus = .failed
+                debug(.service, "Settings import failed with error: \(error)")
             }
         }
     }
@@ -259,8 +245,14 @@ extension Onboarding.StateModel {
     }
 
     enum ImportStatus {
+        case none
         case running
         case finished
         case failed
     }
 }
+
+struct NightscoutImportError: Identifiable {
+    let id = UUID()
+    let message: String
+}

+ 49 - 16
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -61,16 +61,12 @@ extension Onboarding {
                 return false
             }
 
-            debug(.default, "Checking for fresh install in \(documentsURL.path)...")
-
             let expectedLogsFolder = "logs"
             let expectedPreferencesFile = OpenAPS.Settings.preferences
 
             do {
                 let contents = try fileManager.contentsOfDirectory(atPath: documentsURL.path)
 
-                debug(.default, "Found \(contents) in \(documentsURL.path)...")
-
                 // Expect exactly 2 entries: "logs" and the preferences file
                 guard contents.count == 2 else {
                     debug(.default, "Trio install is not fresh; returning user.")
@@ -81,8 +77,6 @@ extension Onboarding {
                 let expectedSet = Set([expectedLogsFolder, expectedPreferencesFile])
                 let actualSet = Set(contents)
 
-                debug(.default, "Expected: \(expectedSet), Actual: \(actualSet)")
-
                 let isFreshInstall = expectedSet == actualSet
                 debug(.default, "Trio install is fresh; new user.")
 
@@ -104,8 +98,8 @@ extension Onboarding {
         var isValidNightscoutURL: Bool = false
         var isConnectingToNS: Bool = false
         var isConnectedToNS: Bool = false
-        var nightscoutImportErrors: [String] = []
-        var nightscoutImportStatus: ImportStatus = .finished
+        var nightscoutImportError: NightscoutImportError?
+        var nightscoutImportStatus: ImportStatus = .none
 
         // MARK: - Units and Pump Omboarding Option
 
@@ -258,10 +252,12 @@ extension Onboarding {
             isConnectingToNS = false
             isValidNightscoutURL = false
 
-            // Attempt to fetch existing units, therapy settings and delivery limits from file
-            units = settingsManager.settings.units
-            fetchExistingTherapySettingsFromFile()
-            fetchExistingDeliveryLimtisFromFile()
+            if !isFreshTrioInstall {
+                // Attempt to fetch existing units, therapy settings and delivery limits from file
+                units = settingsManager.settings.units
+                fetchExistingTherapySettingsFromFile()
+                fetchExistingDeliveryLimtisFromFile()
+            }
         }
 
         // MARK: - Helpers
@@ -417,15 +413,16 @@ extension Onboarding {
         ///   - `units` from app settings.
         func fetchExistingDeliveryLimtisFromFile() {
             let pumpSettingsFromFile = provider.pumpSettingsFromFile
+            let providedSettings = settingsProvider.settings
 
             if let pumpSettingsFromFile = pumpSettingsFromFile {
-                maxBolus = pumpSettingsFromFile.maxBolus
-                maxBasal = pumpSettingsFromFile.maxBasal
+                maxBolus = pumpSettingsFromFile.maxBolus.clamp(to: providedSettings.maxBolus)
+                maxBasal = pumpSettingsFromFile.maxBasal.clamp(to: providedSettings.maxBasal)
             }
 
             let preferences = settingsManager.preferences
-            maxIOB = preferences.maxIOB
-            maxCOB = preferences.maxCOB
+            maxIOB = preferences.maxIOB.clamp(to: providedSettings.maxIOB)
+            maxCOB = preferences.maxCOB.clamp(to: providedSettings.maxCOB)
             minimumSafetyThreshold = preferences.threshold_setting
         }
 
@@ -702,6 +699,30 @@ extension Onboarding {
         func applyToSettings() {
             var settingsCopy = settingsManager.settings
             settingsCopy.units = units
+
+            // ensure existing values cannot exceed new guardrails
+            if !isFreshTrioInstall {
+                let providedSettings = settingsProvider.settings
+
+                settingsCopy.lowGlucose = settingsCopy.lowGlucose.clamp(to: providedSettings.lowGlucose)
+                settingsCopy.highGlucose = settingsCopy.highGlucose.clamp(to: providedSettings.highGlucose)
+                settingsCopy.carbsRequiredThreshold = settingsCopy.carbsRequiredThreshold
+                    .clamp(to: providedSettings.carbsRequiredThreshold)
+                settingsCopy.individualAdjustmentFactor = settingsCopy.individualAdjustmentFactor
+                    .clamp(to: providedSettings.individualAdjustmentFactor)
+                settingsCopy.timeCap = settingsCopy.timeCap.clamp(to: providedSettings.timeCap)
+                settingsCopy.minuteInterval = settingsCopy.minuteInterval.clamp(to: providedSettings.minuteInterval)
+                settingsCopy.delay = settingsCopy.delay.clamp(to: providedSettings.delay)
+                settingsCopy.high = settingsCopy.high.clamp(to: providedSettings.high)
+                settingsCopy.low = settingsCopy.low.clamp(to: providedSettings.low)
+                settingsCopy.maxCarbs = settingsCopy.maxCarbs.clamp(to: providedSettings.maxCarbs)
+                settingsCopy.maxFat = settingsCopy.maxFat.clamp(to: providedSettings.maxFat)
+                settingsCopy.maxProtein = settingsCopy.maxProtein.clamp(to: providedSettings.maxProtein)
+                settingsCopy.overrideFactor = settingsCopy.overrideFactor.clamp(to: providedSettings.overrideFactor)
+                settingsCopy.fattyMealFactor = settingsCopy.fattyMealFactor.clamp(to: providedSettings.fattyMealFactor)
+                settingsCopy.sweetMealFactor = settingsCopy.sweetMealFactor.clamp(to: providedSettings.sweetMealFactor)
+            }
+
             settingsManager.settings = settingsCopy
         }
 
@@ -744,6 +765,18 @@ extension Onboarding {
                 preferences.suspendZerosIOB = true
             }
 
+            // ensure correct bolusIncrement is set, if user is onboarding with paired pump
+            if let pumpManager = apsManager?.pumpManager {
+                let bolusIncrement = Decimal(
+                    pumpManager.supportedBolusVolumes.first ??
+                        Double(
+                            settingsManager.preferences
+                                .bolusIncrement
+                        )
+                )
+                preferences.bolusIncrement = bolusIncrement != 0.025 ? bolusIncrement : 0.1
+            }
+
             settingsManager.preferences = preferences
         }
 

+ 25 - 4
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BluetoothPermissionStepView.swift

@@ -69,6 +69,15 @@ struct BluetoothPermissionStepView: View {
                 allowTitle: String(localized: "Allow"),
                 denyTitle: String(localized: "Don’t Allow"),
                 onAllow: {
+                    /// Requests Bluetooth permission and updates onboarding state based on the system’s response.
+                    /// It calls `authorizeBluetooth`, which initializes `CBCentralManager` and triggers the
+                    /// native system permission prompt (if not previously shown).
+                    ///
+                    /// The resulting authorization is checked — if the user grants permission (`.authorized`),
+                    /// `hasBluetoothGranted` is set to `true`, allowing the app to proceed with Bluetooth operations.
+                    /// Otherwise, it remains `false`, and the user can be guided to manually enable Bluetooth later.
+                    ///
+                    /// This ensures the app only treats Bluetooth as granted when the system confirms it.
                     bluetoothManager.authorizeBluetooth { auth in
                         DispatchQueue.main.async {
                             state.hasBluetoothGranted = (auth == .authorized)
@@ -80,10 +89,22 @@ struct BluetoothPermissionStepView: View {
                     }
                 },
                 onDeny: {
-                    state.hasBluetoothGranted = false
-                    state.shouldDisplayBluetoothRequestAlert = false
-                    if let next = currentStep.wrappedValue.next {
-                        currentStep.wrappedValue = next
+                    /// Requests Bluetooth permission and updates onboarding state based on the system’s response.
+                    /// Although `authorizeBluetooth` is still called (to ensure iOS shows the app under
+                    /// Settings > Privacy & Security > Bluetooth), the app forcibly sets `hasBluetoothGranted` to `false`
+                    /// regardless of the system-reported authorization status.
+                    ///
+                    /// This ensures the app tracks user intent correctly (denial),
+                    /// while still letting the system recognize Bluetooth usage,
+                    /// so users can later re-enable it manually in iOS Settings.
+                    bluetoothManager.authorizeBluetooth { _ in
+                        DispatchQueue.main.async {
+                            state.hasBluetoothGranted = false
+                            state.shouldDisplayBluetoothRequestAlert = false
+                            if let next = currentStep.wrappedValue.next {
+                                currentStep.wrappedValue = next
+                            }
+                        }
                     }
                 }
             )

+ 6 - 5
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DiagnosticsStepView.swift

@@ -3,7 +3,7 @@ import SwiftUI
 struct DiagnosticsStepView: View {
     @Bindable var state: Onboarding.StateModel
 
-    @State private var shouldDisplayPrivacyPolicy: Bool = false
+    @Environment(\.openURL) var openURL
 
     var body: some View {
         VStack(alignment: .leading, spacing: 20) {
@@ -37,7 +37,11 @@ struct DiagnosticsStepView: View {
                 HStack {
                     Text("I have read and accept the")
                     Button("Privacy Policy") {
-                        shouldDisplayPrivacyPolicy = true
+                        if let url = URL(string: "https://github.com/nightscout/Trio/blob/dev/PRIVACY_POLICY.md") {
+                            openURL(url)
+                        } else {
+                            debug(.default, "Invalid URL! Could not gracefully unwrap privacy policy link!")
+                        }
                     }
                     .foregroundColor(.accentColor)
                     .underline()
@@ -86,8 +90,5 @@ struct DiagnosticsStepView: View {
         .onAppear {
             state.syncDiagnosticsOptionFromStorage()
         }
-        .sheet(isPresented: $shouldDisplayPrivacyPolicy) {
-            PrivacyPolicyView()
-        }
     }
 }

+ 15 - 16
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutImportStepView.swift

@@ -2,9 +2,7 @@ import SwiftUI
 
 struct NightscoutImportStepView: View {
     @Bindable var state: Onboarding.StateModel
-
-    @State var importAlert: Alert?
-    @State var isImportAlertPresented: Bool = false
+    @State private var activeImportError: NightscoutImportError?
 
     var body: some View {
         ZStack {
@@ -71,20 +69,21 @@ struct NightscoutImportStepView: View {
             }
         }
         .frame(maxWidth: .infinity, maxHeight: .infinity)
-        .alert(isPresented: $isImportAlertPresented) {
-            if state.nightscoutImportStatus == .failed, state.nightscoutImportErrors.isNotEmpty,
-               let errorMessage = state.nightscoutImportErrors.first
-            {
-                DispatchQueue.main.async {
-                    importAlert = Alert(
-                        title: Text("Import Failed"),
-                        message: Text(errorMessage.description),
-                        dismissButton: .default(Text("OK"))
-                    )
-                    isImportAlertPresented = true
-                }
+        .alert(item: $activeImportError) { error in
+            Alert(
+                title: Text("Import Failed"),
+                message: Text(
+                    error
+                        .message + "\n\n" +
+                        String(localized: "Try again in a moment, or configure your Therapy Settings manually instead.")
+                ),
+                dismissButton: .default(Text("OK"))
+            )
+        }
+        .onChange(of: state.nightscoutImportStatus) { _, newStatus in
+            if newStatus == .failed {
+                activeImportError = state.nightscoutImportError
             }
-            return importAlert ?? Alert(title: Text("Unknown Error"))
         }
     }
 }

+ 21 - 6
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -30,12 +30,27 @@ struct FilteredSettingItem: Identifiable {
 
 enum SettingItems {
     static let trioConfig = [
-        SettingItem(title: "Devices", view: .devices),
-        SettingItem(title: "Therapy", view: .therapySettings),
-        SettingItem(title: "Algorithm", view: .algorithmSettings),
-        SettingItem(title: "Features", view: .featureSettings),
-        SettingItem(title: "Notifications", view: .notificationSettings),
-        SettingItem(title: "Services", view: .serviceSettings)
+        SettingItem(title: String(localized: "Devices", comment: "Devices menu item in the Settings main view."), view: .devices),
+        SettingItem(
+            title: String(localized: "Therapy", comment: "Therapy menu item in the Settings main view."),
+            view: .therapySettings
+        ),
+        SettingItem(
+            title: String(localized: "Algorithm", comment: "Algorithm menu item in the Settings main view."),
+            view: .algorithmSettings
+        ),
+        SettingItem(
+            title: String(localized: "Features", comment: "Features menu item in the Settings main view."),
+            view: .featureSettings
+        ),
+        SettingItem(
+            title: String(localized: "Notifications", comment: "Notifications menu item in the Settings main view."),
+            view: .notificationSettings
+        ),
+        SettingItem(
+            title: String(localized: "Services", comment: "Services menu item in the Settings main view."),
+            view: .serviceSettings
+        )
     ]
 
     static let devicesItems = [

+ 17 - 20
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -78,7 +78,20 @@ extension Settings {
                     Section(
                         header: Text("BRANCH: \(buildDetails.branchAndSha)").textCase(nil),
                         content: {
-                            let versionNumber = Bundle.main.releaseVersionNumber ?? String(localized: "Unknown")
+                            /// The current development version of the app.
+                            ///
+                            /// Follows a semantic pattern where release versions are like `0.5.0`, and
+                            /// development versions increment with a fourth component (e.g., `0.5.0.1`, `0.5.0.2`)
+                            /// after the base release. For example:
+                            /// - After release `0.5.0` → `0.5.0`
+                            /// - First dev push → `0.5.0.1`
+                            /// - Next dev push → `0.5.0.2`
+                            /// - Next release `0.6.0` → `0.6.0`
+                            /// - Next dev push → `0.6.0.1`
+                            ///
+                            /// If the dev version is unavailable, `"unknown"` is returned.
+                            let devVersion = Bundle.main.appDevVersion ?? "unknown"
+
                             let buildNumber = Bundle.main.buildVersionNumber ?? String(localized: "Unknown")
 
                             NavigationLink(destination: SubmodulesView(buildDetails: buildDetails)) {
@@ -90,7 +103,7 @@ extension Settings {
                                         .cornerRadius(10)
                                         .padding(.trailing, 10)
                                     VStack(alignment: .leading, spacing: 4) {
-                                        Text("Trio v\(versionNumber) (\(buildNumber))")
+                                        Text("Trio v\(devVersion) (\(buildNumber))")
                                             .font(.headline)
                                         if let expirationDate = buildDetails.calculateExpirationDate() {
                                             let formattedDate = DateFormatter.localizedString(
@@ -194,7 +207,7 @@ extension Settings {
                             .frame(maxWidth: .infinity, alignment: .leading)
 
                             Button {
-                                if let url = URL(string: "https://discord.gg/FnwFEFUwXE") {
+                                if let url = URL(string: "https://discord.triodocs.org") {
                                     UIApplication.shared.open(url)
                                 }
                             } label: {
@@ -210,7 +223,7 @@ extension Settings {
                             .frame(maxWidth: .infinity, alignment: .leading)
 
                             Button {
-                                if let url = URL(string: "https://m.facebook.com/groups/1351938092206709/") {
+                                if let url = URL(string: "https://facebook.triodocs.org") {
                                     UIApplication.shared.open(url)
                                 }
                             } label: {
@@ -224,22 +237,6 @@ extension Settings {
                                 }
                             }
                             .frame(maxWidth: .infinity, alignment: .leading)
-
-                            Button {
-                                if let url = URL(string: "https://diy-trio.org/") {
-                                    UIApplication.shared.open(url)
-                                }
-                            } label: {
-                                HStack {
-                                    Text("Trio Website")
-                                        .foregroundColor(.primary)
-                                    Spacer()
-                                    Image(systemName: "chevron.right")
-                                        .foregroundColor(.secondary)
-                                        .font(.footnote)
-                                }
-                            }
-                            .frame(maxWidth: .infinity, alignment: .leading)
                         }
                     ).listRowBackground(Color.chart)
 

+ 1 - 1
Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift

@@ -34,7 +34,7 @@ extension Stat.StateModel {
                 // Initially calculate and cache daily averages
                 await calculateAndCacheBolusAveragesAndTotals()
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to setup bolus stats: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to setup bolus stats: \(error)")
             }
         }
     }

+ 1 - 1
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -70,7 +70,7 @@ extension Stat.StateModel {
                     self.loopStats = stats
                 }
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch loop stats: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch loop stats: \(error)")
             }
         }
     }

+ 1 - 1
Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift

@@ -25,7 +25,7 @@ extension Stat.StateModel {
                 // Initially calculate and cache daily averages
                 await calculateAndCacheTDDAverages()
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed fetching TDD stats: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) failed fetching TDD stats: \(error)")
             }
         }
     }

+ 2 - 2
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -135,7 +135,7 @@ extension Stat {
                     return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
                 }
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error)")
                 return []
             }
         }
@@ -148,7 +148,7 @@ extension Stat {
                 glucoseFromPersistence = glucoseObjects
             } catch {
                 debugPrint(
-                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
+                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error)"
                 )
             }
         }

+ 1 - 1
Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -89,7 +89,7 @@ extension TargetsEditor {
                 } catch {
                     debug(
                         .default,
-                        "\(DebuggingIdentifiers.failed) failed to upload targets to Nightscout: \(error.localizedDescription)"
+                        "\(DebuggingIdentifiers.failed) failed to upload targets to Nightscout: \(error)"
                     )
                 }
             }

+ 110 - 23
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -1,6 +1,7 @@
 import Combine
 import CoreData
 import Foundation
+import LocalAuthentication
 import LoopKit
 import Observation
 import SwiftUI
@@ -161,22 +162,26 @@ extension Treatments {
         }
 
         deinit {
-            // Unregister from broadcaster
-            if let broadcaster = broadcaster {
-                broadcaster.unregister(DeterminationObserver.self, observer: self)
-                broadcaster.unregister(BolusFailureObserver.self, observer: self)
-            }
+            debug(.bolusState, "StateModel deinit called")
+        }
 
-            // Cancel Combine subscriptions
-            unsubscribe()
+        private var hasCleanedUp = false
 
+        func cleanupTreatmentState() {
+            guard !hasCleanedUp else { return }
+            hasCleanedUp = true
+
+            unsubscribe()
             bolusProgressCancellable?.cancel()
 
-            debug(.bolusState, "Bolus.StateModel deinitialized")
+            broadcaster?.unregister(DeterminationObserver.self, observer: self)
+            broadcaster?.unregister(BolusFailureObserver.self, observer: self)
+
+            debug(.bolusState, "StateModel cleanup() finished")
         }
 
         private func setupBolusStateConcurrently() {
-            debug(.bolusState, "setupBolusStateConcurrently fired")
+            debug(.bolusState, "Setting up bolus state concurrently...")
             Task {
                 do {
                     try await withThrowingTaskGroup(of: Void.self) { group in
@@ -197,7 +202,7 @@ extension Treatments {
                         try await group.waitForAll()
                     }
                 } catch let error as NSError {
-                    debug(.default, "Failed to setup bolus state concurrently: \(error.localizedDescription)")
+                    debug(.default, "Failed to setup bolus state concurrently: \(error)")
                 }
             }
         }
@@ -453,6 +458,91 @@ extension Treatments {
             }
         }
 
+        /// Returns a user-facing localized error message for a given authentication error.
+        ///
+        /// This function inspects the provided `Error` to determine whether it is an `LAError`,
+        /// and maps its error code to a human-readable, localized string describing the reason
+        /// for the failure. If the error is not an `LAError`, a generic fallback message is returned.
+        ///
+        /// - Parameter error: The `Error` returned from an authentication attempt (e.g., via `LAContext.evaluatePolicy`).
+        /// - Returns: A localized `String` describing the cause of the authentication failure.
+        private func parseAuthenticationError(from error: Error) -> String {
+            guard let laError = error as? LAError else {
+                return String(
+                    localized: "An unknown authentication error occurred. Please try again."
+                )
+            }
+
+            switch laError.code {
+            case .authenticationFailed:
+                return String(
+                    localized: "Authentication failed. Please try again."
+                )
+
+            case .userCancel:
+                return String(
+                    localized: "Authentication was canceled by you."
+                )
+
+            case .userFallback:
+                return String(
+                    localized: "You tapped the fallback option, but no fallback method is configured."
+                )
+
+            case .systemCancel:
+                return String(
+                    localized: "Authentication was canceled by the system. Try again."
+                )
+
+            case .appCancel:
+                return String(
+                    localized: "Authentication was canceled by the app."
+                )
+
+            case .invalidContext:
+                return String(
+                    localized: "Authentication context is invalid. Please try again."
+                )
+
+            case .notInteractive:
+                return String(
+                    localized: "Authentication UI cannot be displayed. Try restarting the app."
+                )
+
+            case .passcodeNotSet:
+                return String(
+                    localized: "Authentication requires a device passcode. Please set one in iOS Settings > Face ID & Passcode."
+                )
+
+            case .biometryNotAvailable:
+                return String(
+                    localized: "Biometric authentication is not available on this device."
+                )
+
+            case .biometryNotEnrolled:
+                return String(
+                    localized: "No biometric identities are enrolled. Please set up Face ID or Touch ID."
+                )
+
+            case .biometryLockout,
+                 .touchIDLockout:
+                return String(
+                    localized: "Biometric authentication is locked due to multiple failed attempts. Please unlock your device using your passcode."
+                )
+
+            case .biometryDisconnected,
+                 .biometryNotPaired:
+                return String(
+                    localized: "Biometric accessory is missing or not connected. Please reconnect it and try again."
+                )
+
+            default:
+                return String(
+                    localized: "An unknown biometric authentication error occurred. Please try again."
+                )
+            }
+        }
+
         func addPumpInsulin() async {
             guard amount > 0 else {
                 showModal(for: nil)
@@ -469,15 +559,14 @@ extension Treatments {
                         self.isAwaitingDeterminationResult = true
                     }
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
-                } else {
-                    print("authentication failed")
                 }
             } catch {
-                print("authentication error for pump bolus: \(error.localizedDescription)")
+                debug(.bolusState, "Authentication error for pump bolus: \(error)")
+
                 await MainActor.run {
                     self.isAwaitingDeterminationResult = false
                     self.showDeterminationFailureAlert = true
-                    self.determinationFailureMessage = error.localizedDescription
+                    self.determinationFailureMessage = parseAuthenticationError(from: error)
                 }
             }
         }
@@ -505,15 +594,13 @@ extension Treatments {
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     // perform determine basal sync
                     try await apsManager.determineBasalSync()
-                } else {
-                    print("authentication failed")
                 }
             } catch {
-                print("authentication error for external insulin: \(error.localizedDescription)")
+                debug(.bolusState, "authentication error for external insulin: \(error)")
                 await MainActor.run {
                     self.isAwaitingDeterminationResult = false
                     self.showDeterminationFailureAlert = true
-                    self.determinationFailureMessage = error.localizedDescription
+                    self.determinationFailureMessage = parseAuthenticationError(from: error)
                 }
             }
         }
@@ -553,7 +640,7 @@ extension Treatments {
                     try await apsManager.determineBasalSync()
                 }
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save carbs: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save carbs: \(error)")
             }
         }
 
@@ -669,7 +756,7 @@ extension Treatments.StateModel {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error)"
                 )
             }
         }
@@ -737,9 +824,9 @@ extension Treatments.StateModel {
 
             updateDeterminationsArray(with: determinationObjects)
         } catch let error as CoreDataError {
-            debug(.default, "Core Data error: \(error.localizedDescription)")
+            debug(.default, "Core Data error: \(error)")
         } catch {
-            debug(.default, "Unexpected error: \(error.localizedDescription)")
+            debug(.default, "Unexpected error: \(error)")
         }
     }
 
@@ -788,7 +875,7 @@ extension Treatments.StateModel {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error)"
             )
             return nil
         }

+ 5 - 2
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -388,6 +388,9 @@ extension Treatments {
             .onDisappear {
                 state.isActive = false
                 state.addButtonPressed = false
+
+                // Cancel all Combine subscriptions and unregister State from broadcaster
+                state.cleanupTreatmentState()
             }
             .sheet(isPresented: $state.showInfo) {
                 PopupView(state: state)
@@ -397,12 +400,12 @@ extension Treatments {
             }) {
                 MealPresetView(state: state)
             }
-            .alert("Determination Failed", isPresented: $state.showDeterminationFailureAlert) {
+            .alert("Error while processing Treatment", isPresented: $state.showDeterminationFailureAlert) {
                 Button("OK", role: .cancel) {
                     state.hideModal()
                 }
             } message: {
-                Text("Failed to update COB/IOB: \(state.determinationFailureMessage)")
+                Text("\(state.determinationFailureMessage)")
             }
         }
 

+ 2 - 2
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -361,7 +361,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Error preparing calculation input: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error preparing calculation input: \(error)"
             )
             // Return default values in case of error
             throw error
@@ -504,7 +504,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Error in bolus calculation: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error in bolus calculation: \(error)"
             )
             // Return safe default values
             return CalculationResult(

+ 1 - 1
Trio/Sources/Services/Calendar/CalendarManager.swift

@@ -307,7 +307,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
 
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create calendar event: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create calendar event: \(error)"
             )
         }
     }

+ 7 - 7
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -165,7 +165,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         } catch {
             debug(
                 .service,
-                "\(DebuggingIdentifiers.failed) Error fetching glucose for health upload: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error fetching glucose for health upload: \(error)"
             )
         }
     }
@@ -209,7 +209,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             await updateGlucoseAsUploaded(glucose)
 
         } catch {
-            debug(.service, "Failed to upload glucose samples to HealthKit: \(error.localizedDescription)")
+            debug(.service, "Failed to upload glucose samples to HealthKit: \(error)")
         }
     }
 
@@ -244,7 +244,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         } catch {
             debug(
                 .service,
-                "\(DebuggingIdentifiers.failed) Error fetching carbs for health upload: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error fetching carbs for health upload: \(error)"
             )
         }
     }
@@ -330,7 +330,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             await updateCarbsAsUploaded(carbs)
 
         } catch {
-            debug(.service, "Failed to upload carb samples to HealthKit: \(error.localizedDescription)")
+            debug(.service, "Failed to upload carb samples to HealthKit: \(error)")
         }
     }
 
@@ -365,7 +365,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         } catch {
             debug(
                 .service,
-                "\(DebuggingIdentifiers.failed) Error fetching insulin events for health upload: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error fetching insulin events for health upload: \(error)"
             )
         }
     }
@@ -458,10 +458,10 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                 debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
                 await updateInsulinAsUploaded(insulinEvents)
             } catch {
-                debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
+                debug(.service, "Failed to upload insulin samples to HealthKit: \(error)")
             }
         } catch {
-            debug(.service, "\(DebuggingIdentifiers.failed) Error fetching temp basal entries: \(error.localizedDescription)")
+            debug(.service, "\(DebuggingIdentifiers.failed) Error fetching temp basal entries: \(error)")
         }
     }
 

+ 27 - 10
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -167,7 +167,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error)"
                 )
             }
         }
@@ -182,7 +182,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                     await self.updateContentState(determination)
                 }
             } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch and map override: \(error.localizedDescription)")
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch and map override: \(error)")
             }
         }
     }
@@ -241,7 +241,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
 
     /// Triggers an update of the live activity order.
     ///
-    /// This method refreshes the activitys content state to reflect any changes in the widget order.
+    /// This method refreshes the activity's content state to reflect any changes in the widget order.
     @MainActor private func updateLiveActivityOrder() async {
         Task {
             await updateContentState(determination)
@@ -296,27 +296,44 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     ///
     /// - Parameter state: The new content state to push to the live activity.
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
+        // End all unknown activities except the current one
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
         {
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
 
-        if let currentActivity = currentActivity {
+        // Defensive: capture the current activity at function start
+        let activityAtStart = currentActivity
+
+        if let currentActivity = activityAtStart {
             if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
+                debug(.default, "[LiveActivityManager] Ending current activity for recreation: \(currentActivity.activity.id)")
                 await endActivity()
-                Task { @MainActor in
-                    await self.pushUpdate(state)
+                // After endActivity(), currentActivity is guaranteed to be nil
+                // No recursive task, but explicitly restart
+                if self.currentActivity == nil {
+                    debug(.default, "[LiveActivityManager] Re-pushing update after recreation.")
+                    await pushUpdate(state)
+                } else {
+                    debug(.default, "[LiveActivityManager] Warning: currentActivity was not nil after endActivity!")
                 }
                 return
             } else {
                 let content = ActivityContent(
                     state: state,
-                    staleDate: min(state.date ?? Date.now, Date.now).addingTimeInterval(360) // 6 minutes in seconds
+                    staleDate: min(state.date ?? Date.now, Date.now).addingTimeInterval(360)
                 )
-                await currentActivity.activity.update(content)
+                // Before the update, check if currentActivity is still valid
+                if let stillCurrent = self.currentActivity, stillCurrent.activity.id == currentActivity.activity.id {
+                    debug(.default, "[LiveActivityManager] Updating current activity: \(stillCurrent.activity.id)")
+                    await stillCurrent.activity.update(content)
+                } else {
+                    debug(.default, "[LiveActivityManager] Skipped update: currentActivity changed during pushUpdate.")
+                }
             }
         } else {
+            // ... Activity is newly created ...
             do {
                 let expired = ActivityContent(
                     state: LiveActivityAttributes
@@ -342,7 +359,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                     pushType: nil
                 )
                 currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
-
+                debug(.default, "[LiveActivityManager] Created new activity: \(activity.id)")
                 await pushUpdate(state)
             } catch {
                 debug(
@@ -378,7 +395,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
 
     /// Restarts the live activity from a Live Activity Intent.
     ///
-    /// This method mimics xdrips `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
+    /// This method mimics xdrip's `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
     /// ending the current live activity, and starting a new one using the current state.
     @MainActor func restartActivityFromLiveActivityIntent() async {
         guard let latestGlucose = latestGlucose,

+ 9 - 9
Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -93,7 +93,7 @@ extension NightscoutAPI {
                 return reading
             }
         } catch {
-            warning(.nightscout, "Glucose fetching error: \(error.localizedDescription)")
+            warning(.nightscout, "Glucose fetching error: \(error)")
             return []
         }
     }
@@ -141,7 +141,7 @@ extension NightscoutAPI {
             let carbs = try JSONCoding.decoder.decode([CarbsEntry].self, from: data)
             return carbs
         } catch {
-            warning(.nightscout, "Carbs fetching error: \(error.localizedDescription)")
+            warning(.nightscout, "Carbs fetching error: \(error)")
             throw error
         }
     }
@@ -281,7 +281,7 @@ extension NightscoutAPI {
             let tempTargets = try JSONCoding.decoder.decode([TempTarget].self, from: data)
             return tempTargets
         } catch {
-            warning(.nightscout, "TempTarget fetching error: \(error.localizedDescription)")
+            warning(.nightscout, "TempTarget fetching error: \(error)")
             throw error
         }
     }
@@ -312,7 +312,7 @@ extension NightscoutAPI {
 //            debugPrint("Payload treatments size: \(encodedBody.count) bytes")
 //            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
-            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            debugPrint("Error encoding payload: \(error)")
             throw error
         }
         request.httpMethod = "POST"
@@ -348,7 +348,7 @@ extension NightscoutAPI {
 //            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
 //            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
-            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            debugPrint("Error encoding payload: \(error)")
             throw error
         }
         request.httpMethod = "POST"
@@ -385,7 +385,7 @@ extension NightscoutAPI {
 //            debugPrint("Payload status size: \(encodedBody.count) bytes")
 //            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
-            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            debugPrint("Error encoding payload: \(error)")
             throw error
         }
 
@@ -424,7 +424,7 @@ extension NightscoutAPI {
 //            debugPrint("Payload profile upload size: \(encodedBody.count) bytes")
 //            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
-            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            debugPrint("Error encoding payload: \(error)")
             throw error
         }
         request.httpMethod = "POST"
@@ -489,7 +489,7 @@ extension NightscoutAPI {
 //            debugPrint("Payload glucose size: \(encodedBody.count) bytes")
 //            debugPrint(String(data: encodedBody, encoding: .utf8) ?? "Invalid payload")
         } catch {
-            debugPrint("Error encoding payload: \(error.localizedDescription)")
+            debugPrint("Error encoding payload: \(error)")
             throw error
         }
         request.httpMethod = "POST"
@@ -546,7 +546,7 @@ extension NightscoutAPI {
 
             return fetchedProfile
         } catch {
-            warning(.nightscout, "Could not fetch Nightscout Profile! Error: \(error.localizedDescription)")
+            warning(.nightscout, "Could not fetch Nightscout Profile! Error: \(error)")
             throw error
         }
     }

+ 24 - 36
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -115,7 +115,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) failed to fetch last enacted determination: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) failed to fetch last enacted determination: \(error)"
                 )
             }
         }
@@ -133,19 +133,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         /// 2. To not spam the user's NS site with a high number of uploads in a very short amount of time (less than 1sec)
         coreDataPublisher?
             .filteredByEntityName("OrefDetermination")
-            .handleEvents(receiveOutput: { _ in
-                debug(
-                    .nightscout,
-                    "OrefDetermination update"
-                )
-            })
             .debounce(for: .seconds(2), scheduler: debouncedQueue)
-            .handleEvents(receiveOutput: { _ in
-                debug(
-                    .nightscout,
-                    "OrefDetermination update debounceed"
-                )
-            })
             .sink { [weak self] objectIDs in
                 guard let self = self else { return }
 
@@ -398,7 +386,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             let carbs = try await nightscout.fetchCarbs(sinceDate: since)
             return carbs
         } catch {
-            debug(.nightscout, "Error fetching carbs: \(error.localizedDescription)")
+            debug(.nightscout, "Error fetching carbs: \(error)")
             return []
         }
     }
@@ -413,7 +401,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             let tempTargets = try await nightscout.fetchTempTargets(sinceDate: since)
             return tempTargets
         } catch {
-            debug(.nightscout, "Error fetching temp targets: \(error.localizedDescription)")
+            debug(.nightscout, "Error fetching temp targets: \(error)")
             return []
         }
     }
@@ -427,7 +415,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) Failed to delete Carbs from Nightscout with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to delete Carbs from Nightscout with error: \(error)"
             )
         }
     }
@@ -441,7 +429,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) Failed to delete Insulin from Nightscout with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to delete Insulin from Nightscout with error: \(error)"
             )
         }
     }
@@ -454,7 +442,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error)"
             )
         }
     }
@@ -662,7 +650,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 self.lastSuggestedDetermination = lastSuggestedDetermination
             }
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
 
         Task.detached {
@@ -851,7 +839,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
                 debug(.nightscout, "Profile uploaded")
             } catch {
-                debug(.nightscout, "NightscoutManager uploadProfile: \(error.localizedDescription)")
+                debug(.nightscout, "NightscoutManager uploadProfile: \(error)")
                 throw error
             }
         } else {
@@ -868,7 +856,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         do {
             return try await nightscout.importSettings()
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
             return nil
         }
     }
@@ -880,7 +868,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload glucose with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) failed to upload glucose with error: \(error)"
             )
         }
     }
@@ -891,7 +879,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload manual glucose with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) failed to upload manual glucose with error: \(error)"
             )
         }
     }
@@ -902,7 +890,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload pump history with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) failed to upload pump history with error: \(error)"
             )
         }
     }
@@ -914,7 +902,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload carbs with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) failed to upload carbs with error: \(error)"
             )
         }
     }
@@ -926,7 +914,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload overrides with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) failed to upload overrides with error: \(error)"
             )
         }
     }
@@ -938,7 +926,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload temp targets with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) failed to upload temp targets with error: \(error)"
             )
         }
     }
@@ -959,7 +947,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Glucose uploaded")
         } catch {
-            debug(.nightscout, "Upload of glucose failed: \(error.localizedDescription)")
+            debug(.nightscout, "Upload of glucose failed: \(error)")
         }
     }
 
@@ -997,7 +985,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Treatments uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1015,7 +1003,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Treatments uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1056,7 +1044,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Treatments uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1097,7 +1085,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Treatments uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1156,7 +1144,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Overrides uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1214,7 +1202,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Overrides uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1255,7 +1243,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Temp Targets uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 
@@ -1296,7 +1284,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
             debug(.nightscout, "Temp Target Runs uploaded")
         } catch {
-            debug(.nightscout, error.localizedDescription)
+            debug(.nightscout, String(describing: error))
         }
     }
 

+ 4 - 4
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -191,7 +191,7 @@ extension BaseTidepoolManager {
         do {
             try uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
         } catch {
-            debug(.service, "\(DebuggingIdentifiers.failed) Failed to upload carbs with error: \(error.localizedDescription)")
+            debug(.service, "\(DebuggingIdentifiers.failed) Failed to upload carbs with error: \(error)")
         }
     }
 
@@ -283,7 +283,7 @@ extension BaseTidepoolManager {
             let events = try await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool()
             await uploadDose(events)
         } catch {
-            debug(.service, "Error fetching pump history: \(error.localizedDescription)")
+            debug(.service, "Error fetching pump history: \(error)")
         }
     }
 
@@ -401,7 +401,7 @@ extension BaseTidepoolManager {
                 }
             }
         } catch {
-            debug(.service, "Error fetching temp basal entries: \(error.localizedDescription)")
+            debug(.service, "Error fetching temp basal entries: \(error)")
         }
     }
 
@@ -596,7 +596,7 @@ extension BaseTidepoolManager {
             let manualGlucose = try await glucoseStorage.getManualGlucoseNotYetUploadedToTidepool()
             uploadGlucose(manualGlucose)
         } catch {
-            debug(.service, "Error fetching glucose data: \(error.localizedDescription)")
+            debug(.service, "Error fetching glucose data: \(error)")
         }
     }
 

+ 3 - 3
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift

@@ -35,7 +35,7 @@ extension TrioRemoteControl {
         } catch {
             debug(
                 .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error)"
             )
             await logError(
                 "Command failed: \(error.localizedDescription)",
@@ -61,7 +61,7 @@ extension TrioRemoteControl {
                 debug(.remoteControl, "Remote command processed successfully. \(pushMessage.humanReadableDescription())")
             }
         } catch {
-            debug(.remoteControl, "Failed to enact override preset: \(error.localizedDescription)")
+            debug(.remoteControl, "Failed to enact override preset: \(error)")
         }
     }
 
@@ -111,7 +111,7 @@ extension TrioRemoteControl {
         } catch {
             debug(
                 .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error)"
             )
         }
     }

+ 1 - 1
Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -94,7 +94,7 @@ extension TrioRemoteControl {
         } catch {
             debug(
                 .remoteControl,
-                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error)"
             )
             await logError("Failed to disable temp targets: \(error.localizedDescription)")
         }

+ 11 - 11
Trio/Sources/Services/Storage/FileStorage.swift

@@ -30,7 +30,7 @@ final class BaseFileStorage: FileStorage {
                     try Disk.save(value, to: .documents, as: name, encoder: JSONCoding.encoder)
                 }
             } catch {
-                debug(.storage, "Failed to save file '\(name)': \(error.localizedDescription)")
+                debug(.storage, "Failed to save file '\(name)': \(error)")
             }
         }
     }
@@ -45,7 +45,7 @@ final class BaseFileStorage: FileStorage {
                         try Disk.save(value, to: .documents, as: name, encoder: JSONCoding.encoder)
                     }
                 } catch {
-                    debug(.storage, "Failed to save file '\(name)': \(error.localizedDescription)")
+                    debug(.storage, "Failed to save file '\(name)': \(error)")
                 }
                 continuation.resume()
             }
@@ -57,7 +57,7 @@ final class BaseFileStorage: FileStorage {
             do {
                 return try Disk.retrieve(name, from: .documents, as: type, decoder: JSONCoding.decoder)
             } catch {
-                debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
+                debug(.storage, "Failed to retrieve file '\(name)': \(error)")
                 return nil
             }
         }
@@ -70,7 +70,7 @@ final class BaseFileStorage: FileStorage {
                     let result = try Disk.retrieve(name, from: .documents, as: type, decoder: JSONCoding.decoder)
                     continuation.resume(returning: result)
                 } catch {
-                    debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
+                    debug(.storage, "Failed to retrieve file '\(name)': \(error)")
                     continuation.resume(returning: nil)
                 }
             }
@@ -87,7 +87,7 @@ final class BaseFileStorage: FileStorage {
                 }
                 return string
             } catch {
-                debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
+                debug(.storage, "Failed to retrieve file '\(name)': \(error)")
                 return nil
             }
         }
@@ -105,7 +105,7 @@ final class BaseFileStorage: FileStorage {
                     }
                     continuation.resume(returning: string)
                 } catch {
-                    debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
+                    debug(.storage, "Failed to retrieve file '\(name)': \(error)")
                     continuation.resume(returning: nil)
                 }
             }
@@ -117,7 +117,7 @@ final class BaseFileStorage: FileStorage {
             do {
                 try Disk.append(newValue, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder)
             } catch {
-                debug(.storage, "Failed to append to file '\(name)': \(error.localizedDescription)")
+                debug(.storage, "Failed to append to file '\(name)': \(error)")
             }
         }
     }
@@ -127,7 +127,7 @@ final class BaseFileStorage: FileStorage {
             do {
                 try Disk.append(newValues, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder)
             } catch {
-                debug(.storage, "Failed to append to file '\(name)': \(error.localizedDescription)")
+                debug(.storage, "Failed to append to file '\(name)': \(error)")
             }
         }
     }
@@ -173,7 +173,7 @@ final class BaseFileStorage: FileStorage {
             do {
                 try Disk.remove(name, from: .documents)
             } catch {
-                debug(.storage, "Failed to remove file '\(name)': \(error.localizedDescription)")
+                debug(.storage, "Failed to remove file '\(name)': \(error)")
             }
         }
     }
@@ -183,7 +183,7 @@ final class BaseFileStorage: FileStorage {
             do {
                 try Disk.rename(name, in: .documents, to: newName)
             } catch {
-                debug(.storage, "Failed to rename file '\(name)' to '\(newName)': \(error.localizedDescription)")
+                debug(.storage, "Failed to rename file '\(name)' to '\(newName)': \(error)")
             }
         }
     }
@@ -198,7 +198,7 @@ final class BaseFileStorage: FileStorage {
         do {
             return try Disk.url(for: file, in: .documents)
         } catch {
-            debug(.storage, "Failed to get URL for file '\(file)': \(error.localizedDescription)")
+            debug(.storage, "Failed to get URL for file '\(file)': \(error)")
             return nil
         }
     }

+ 1 - 5
Trio/Sources/Services/UnlockManager/UnlockManager.swift

@@ -5,10 +5,6 @@ protocol UnlockManager {
     func unlock() async throws -> Bool
 }
 
-struct UnlockError: Error {
-    let error: Error?
-}
-
 final class BaseUnlockManager: UnlockManager {
     @MainActor func unlock() async throws -> Bool {
         let context = LAContext()
@@ -18,7 +14,7 @@ final class BaseUnlockManager: UnlockManager {
             _ = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
             return true
         } catch {
-            throw UnlockError(error: error)
+            throw error
         }
     }
 }

+ 1 - 1
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -326,7 +326,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to send glucose notification with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to send glucose notification with error: \(error)"
             )
         }
     }

+ 42 - 21
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -327,7 +327,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         } catch {
             debug(
                 .watchManager,
-                "\(DebuggingIdentifiers.failed) Error setting up watch state: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error setting up watch state: \(error)"
             )
             // Return empty state in case of error
             return WatchState(date: Date())
@@ -388,7 +388,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Error getting active bolus amount: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error getting active bolus amount: \(error)"
             )
         }
     }
@@ -471,7 +471,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         // if session is not reachable, it means it's in background -> send watchState as userInfo
         if session.isReachable {
             session.sendMessage([WatchMessageKeys.watchState: message], replyHandler: nil) { error in
-                debug(.watchManager, "❌ Error sending watch state: \(error.localizedDescription)")
+                debug(.watchManager, "❌ Error sending watch state: \(error)")
             }
             WatchStateSnapshot.saveLatestDateToDisk(state.date)
         } else {
@@ -494,7 +494,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         ]
 
         session.sendMessage(ackMessage, replyHandler: nil) { error in
-            debug(.watchManager, "❌ Error sending acknowledgment: \(error.localizedDescription)")
+            debug(.watchManager, "❌ Error sending acknowledgment: \(error)")
         }
     }
 
@@ -502,7 +502,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
         if let error = error {
-            debug(.watchManager, "📱 Phone session activation failed: \(error.localizedDescription)")
+            debug(.watchManager, "📱 Phone session activation failed: \(error)")
             return
         }
 
@@ -611,9 +611,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         }
 
                     } catch let error as CoreDataError {
-                        debug(.default, "Core Data error: \(error.localizedDescription)")
+                        debug(.default, "Core Data error: \(error)")
                     } catch {
-                        debug(.default, "Unexpected error: \(error.localizedDescription)")
+                        debug(.default, "Unexpected error: \(error)")
                     }
 
                     // Get recommendation from BolusCalculationManager
@@ -726,7 +726,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         ackCode: .carbsLogged
                     )
                 } catch {
-                    debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
+                    debug(.watchManager, "❌ Error saving carbs: \(error)")
 
                     // Acknowledge failure
                     self.sendAcknowledgment(toWatch: false, message: "Error logging carbs", ackCode: .genericFailure)
@@ -748,7 +748,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 // Notify Watch: "Saving carbs..."
                 self.sendAcknowledgment(
                     toWatch: true,
-                    message: String(localized: "Saving Carbs...", comment: "Successful message sent to watch when saving carbs"),
+                    message: String(
+                        localized: "Saving Carbs...",
+                        comment: "Successful message sent to watch when saving carbs"
+                    ),
                     ackCode: .savingCarbs
                 )
 
@@ -807,7 +810,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 )
 
             } catch {
-                debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
+                debug(.watchManager, "❌ Error processing combined request: \(error)")
                 sendAcknowledgment(toWatch: false, message: "Failed to log carbs and bolus", ackCode: .genericFailure)
             }
         }
@@ -848,11 +851,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                             // Acknowledge cancellation success
                             self.sendAcknowledgment(
                                 toWatch: true,
-                                message: "Stopped Override successfully.",
+                                message: String(
+                                    localized: "Stopped Override successfully.",
+                                    comment: "Stopped Override successfully"
+                                ),
                                 ackCode: .overrideStopped
                             )
                         } catch {
-                            debug(.watchManager, "❌ Error cancelling override: \(error.localizedDescription)")
+                            debug(.watchManager, "❌ Error cancelling override: \(error)")
                             // Acknowledge cancellation error
                             self.sendAcknowledgment(toWatch: false, message: "Error stopping Override.", ackCode: .genericFailure)
                         }
@@ -900,7 +906,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     debug(.watchManager, "📱 Currently no override is active... proceeding to activate override: \(presetName)")
                 }
             } catch {
-                debug(.watchManager, "❌ Error while checking for active override: \(error.localizedDescription)")
+                debug(.watchManager, "❌ Error while checking for active override: \(error)")
                 self.sendAcknowledgment(
                     toWatch: false,
                     message: "Failed to load active override.",
@@ -917,7 +923,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     debug(.watchManager, "❌ No matching preset found for name: \"\(presetName)\" in \(presets.map(\.name))")
                     self.sendAcknowledgment(
                         toWatch: false,
-                        message: "Preset not found: \(presetName)",
+                        message: String(
+                            localized: "Preset \"\(presetName)\" not found.",
+                            comment: "Preset not found"
+                        ),
                         ackCode: .genericFailure
                     )
                     return
@@ -931,7 +940,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         // Acknowledge failure
                         self.sendAcknowledgment(
                             toWatch: false,
-                            message: "Error! Something went wrong when processing your request.",
+                            message: String(
+                                localized: "Error! Something went wrong when processing your request.",
+                                comment: "Error message when activating override"
+                            ),
                             ackCode: .genericFailure
                         )
                         return
@@ -948,11 +960,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     // Acknowledge activation success
                     self.sendAcknowledgment(
                         toWatch: true,
-                        message: "Started Override \"\(presetName)\" successfully.",
+                        message: String(
+                            localized: "Started Override \"\(presetName)\" successfully.",
+                            comment: "Start override with override name"
+                        ),
                         ackCode: .overrideStarted
                     )
                 } catch {
-                    debug(.watchManager, "❌ Error activating override: \(error.localizedDescription)")
+                    debug(.watchManager, "❌ Error activating override: \(error)")
                     // Acknowledge activation error
                     self.sendAcknowledgment(
                         toWatch: false,
@@ -1036,11 +1051,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         // Acknowledge activation success
                         self.sendAcknowledgment(
                             toWatch: true,
-                            message: "Started Temp Target \"\(presetName)\" successfully.",
+                            message: String(
+                                localized: "Started Temp Target \"\(presetName)\" successfully.",
+                                comment: "Started Temp Target successfully."
+                            ),
                             ackCode: .tempTargetStarted
                         )
                     } catch {
-                        debug(.watchManager, "❌ Error activating temp target: \(error.localizedDescription)")
+                        debug(.watchManager, "❌ Error activating temp target: \(error)")
                         // Acknowledge activation error
                         self.sendAcknowledgment(
                             toWatch: false,
@@ -1091,11 +1109,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                             // Acknowledge cancellation success
                             self.sendAcknowledgment(
                                 toWatch: true,
-                                message: "Stopped Temp Target successfully.",
+                                message: String(
+                                    localized: "Stopped Temp Target successfully.",
+                                    comment: "Stopped Temp Target successfully."
+                                ),
                                 ackCode: .tempTargetStopped
                             )
                         } catch {
-                            debug(.watchManager, "❌ Error stopping temp target: \(error.localizedDescription)")
+                            debug(.watchManager, "❌ Error stopping temp target: \(error)")
                             // Acknowledge cancellation error
                             self.sendAcknowledgment(
                                 toWatch: false,

+ 5 - 5
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -141,7 +141,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
                     } catch {
                         debug(
                             .watchManager,
-                            "\(DebuggingIdentifiers.failed) Error updating watch state: \(error.localizedDescription)"
+                            "\(DebuggingIdentifiers.failed) Error updating watch state: \(error)"
                         )
                     }
                 }
@@ -168,7 +168,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
                     } catch {
                         debug(
                             .watchManager,
-                            "\(DebuggingIdentifiers.failed) failed to update watch state: \(error.localizedDescription)"
+                            "\(DebuggingIdentifiers.failed) failed to update watch state: \(error)"
                         )
                     }
                 }
@@ -188,7 +188,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
                     } catch {
                         debug(
                             .watchManager,
-                            "\(DebuggingIdentifiers.failed) failed to update watch state: \(error.localizedDescription)"
+                            "\(DebuggingIdentifiers.failed) failed to update watch state: \(error)"
                         )
                     }
                 }
@@ -315,7 +315,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
         } catch {
             debug(
                 .watchManager,
-                "\(DebuggingIdentifiers.failed) Error setting up Garmin watch state: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error setting up Garmin watch state: \(error)"
             )
             throw error
         }
@@ -610,7 +610,7 @@ extension BaseGarminManager: SettingsObserver {
             } catch {
                 debug(
                     .watchManager,
-                    "\(DebuggingIdentifiers.failed) failed to send watch state data: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) failed to send watch state data: \(error)"
                 )
             }
         }

+ 4 - 4
Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift

@@ -37,7 +37,7 @@ import UIKit
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Error fetching/processing overrides: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Error fetching/processing overrides: \(error)"
             )
             throw error
         }
@@ -72,7 +72,7 @@ import UIKit
             } catch {
                 debug(
                     .default,
-                    "\(DebuggingIdentifiers.failed) Failed to fetch Override: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) Failed to fetch Override: \(error)"
                 )
                 throw error
             }
@@ -145,7 +145,7 @@ import UIKit
         } catch {
             debug(
                 .default,
-                "\(DebuggingIdentifiers.failed) Failed to enact override: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) Failed to enact override: \(error)"
             )
             endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Enact")
             return false
@@ -231,7 +231,7 @@ import UIKit
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error)"
             )
             if var backgroundTaskID = backgroundTaskID {
                 debug(.default, "Ending background task for override cancel")

+ 4 - 4
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -73,7 +73,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
                 }
             } catch let error as NSError {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch TempTarget: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch TempTarget: \(error)"
                 )
                 return [TempPreset(id: UUID(), name: "", duration: 0)]
             }
@@ -94,7 +94,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
                 return try self.coredataContext.fetch(fetchRequest).first?.objectID
             } catch {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Target: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Target: \(error)"
                 )
                 return nil
             }
@@ -169,7 +169,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
             return intentSuccess
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Temp Target with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Temp Target with error: \(error)"
             )
 
             endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Enact")
@@ -246,7 +246,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Temp Targets with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Temp Targets with error: \(error)"
             )
             if var backgroundTaskID = backgroundTaskID {
                 debug(.default, "Ending background task for temp target cancel")

+ 77 - 0
TrioTests/LocalizationTests.swift

@@ -0,0 +1,77 @@
+
+import Foundation
+import Testing
+
+private let bundle = Bundle.main
+
+@Suite("Localization Tests", .serialized) struct LocalizationTests {
+    @Test("No stray % inside format strings") func testNoStrayPercent() {
+        // Array to collect strings with issues
+        var offenders: [(lang: String, key: String, value: String, file: String)] = []
+
+        // Regular expression patterns
+        let placeholderPattern = "%[0-9]*\\$?[.,]?[0-9]*[a-zA-Z@]" // Matches placeholders like %@, %d, %1$@
+        let escapedPercentPattern = "%%" // Matches escaped percent signs
+        let percentPattern = "%" // Matches any percent sign
+
+        // Compile regexes (force-unwrapped since patterns are static and valid)
+        let placeholderRegex = try! NSRegularExpression(pattern: placeholderPattern)
+        let escapedPercentRegex = try! NSRegularExpression(pattern: escapedPercentPattern)
+        let percentRegex = try! NSRegularExpression(pattern: percentPattern)
+
+        // Assume 'bundle' is accessible, e.g., Bundle.main
+        for locale in bundle.localizations where locale != "Base" {
+            guard let lproj = bundle.path(forResource: locale, ofType: "lproj"),
+                  let files = FileManager.default.enumerator(atPath: lproj) else { continue }
+
+            // Iterate over .strings files in the localization directory
+            for case let f as String in files where f.hasSuffix(".strings") {
+                let path = (lproj as NSString).appendingPathComponent(f)
+                guard let table = NSDictionary(contentsOfFile: path) as? [String: String] else { continue }
+
+                // Check each key-value pair in the .strings file
+                for (key, value) in table {
+                    let nsValue = value as NSString
+                    let range = NSRange(location: 0, length: nsValue.length)
+
+                    // Determine if the value contains any placeholders
+                    let hasPlaceholders = placeholderRegex.firstMatch(in: value, range: range) != nil
+
+                    // Only check for stray % if the value has placeholders
+                    if hasPlaceholders {
+                        // Find all ranges covered by placeholders and escaped %%
+                        let placeholderMatches = placeholderRegex.matches(in: value, range: range)
+                        let escapedMatches = escapedPercentRegex.matches(in: value, range: range)
+                        let coveredRanges = (placeholderMatches + escapedMatches).map(\.range)
+
+                        // Find all % signs in the value
+                        let percentMatches = percentRegex.matches(in: value, range: range)
+
+                        // Check each % to see if it's stray (not covered by a placeholder or %%)
+                        for percentMatch in percentMatches {
+                            let percentLocation = percentMatch.range.location
+                            let isCovered = coveredRanges.contains { NSLocationInRange(percentLocation, $0) }
+                            if !isCovered {
+                                offenders.append((lang: locale, key: key, value: value, file: f))
+                                break // Stop checking this string after finding an issue
+                            }
+                        }
+                    }
+                    // If no placeholders, skip the check (single % is allowed)
+                }
+            }
+        }
+
+        // Assert that no offenders were found using Testing's #expect
+        #expect(
+            offenders.isEmpty,
+            """
+            Found \(offenders.count) string(s) that still have a single % although \
+            the value contains printf placeholders:
+
+            \(offenders.map { "\($0.lang) – \($0.file)\n⟨key⟩   \($0.key)\n⟨value⟩ \($0.value)" }
+                .joined(separator: "\n\n"))
+            """
+        )
+    }
+}