Преглед изворни кода

Merge branch 'dev' of github.com:nightscout/Trio into backdatingCarbs

Marvin Polscheit пре 1 година
родитељ
комит
e56c850af3
29 измењених фајлова са 14690 додато и 839 уклоњено
  1. 1 1
      Config.xcconfig
  2. 1 1
      DanaKit
  3. 34 31
      README.md
  4. 15 3
      Trio.xcodeproj/project.pbxproj
  5. 1 1
      Trio/Sources/APS/APSManager.swift
  6. 1 1
      Trio/Sources/APS/DeviceDataManager.swift
  7. 7 0
      Trio/Sources/APS/Extensions/DecimalExtensions.swift
  8. 3 3
      Trio/Sources/APS/FetchGlucoseManager.swift
  9. 1 1
      Trio/Sources/APS/OpenAPS/Script.swift
  10. 18 10
      Trio/Sources/APS/Storage/CarbsStorage.swift
  11. 1 1
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  12. 42 0
      Trio/Sources/Helpers/TimeAgoFormatter.swift
  13. 14263 698
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  14. 35 1
      Trio/Sources/Logger/Logger.swift
  15. 1 1
      Trio/Sources/Models/DecimalPickerSettings.swift
  16. 6 11
      Trio/Sources/Models/TrioSettings.swift
  17. 3 1
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  18. 3 1
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  19. 1 1
      Trio/Sources/Modules/Home/HomeStateModel.swift
  20. 1 11
      Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  21. 2 6
      Trio/Sources/Modules/Home/View/Header/LoopView.swift
  22. 18 1
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  23. 1 1
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  24. 13 24
      Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  25. 47 14
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  26. 91 8
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  27. 2 2
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  28. 1 5
      Trio/Sources/Services/UnlockManager/UnlockManager.swift
  29. 77 0
      TrioTests/LocalizationTests.swift

+ 1 - 1
Config.xcconfig

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

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 89062b019687a61976a077293ee5a3928cf63900
+Subproject commit ee9ebdd880fdcc9bc50885e60408b7c64f8834d1

+ 34 - 31
README.md

@@ -16,7 +16,7 @@ You can either use the Build Script or you can run each command manually.
 
 
 ### Build Script:
 ### 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 \
 /bin/bash -c "$(curl -fsSL \
@@ -34,67 +34,70 @@ 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:
 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.
 Copy the command below, and replace `xxxxxxxxxx` by your Apple Developer ID before running the command in Terminal.
+
 ```
 ```
 echo 'DEVELOPER_TEAM = xxxxxxxxxx' > ConfigOverride.xcconfig
 echo 'DEVELOPER_TEAM = xxxxxxxxxx' > ConfigOverride.xcconfig
 ```
 ```
 
 
 Then launch Xcode and build the Trio app:
 Then launch Xcode and build the Trio app:
+
 ```
 ```
 xed .
 xed .
 ```
 ```
 
 
 ## To build directly in GitHub, without using Xcode:
 ## 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:
 ## Please understand that Trio is:
+
 - an open-source system developed by enthusiasts and for use at your own risk
 - an open-source system developed by enthusiasts and for use at your own risk
 - not CE or FDA approved for therapy.
 - not CE or FDA approved for therapy.
 
 
+## Documentation
 
 
-# Documentation
-
-[Discord Trio - Server ](http://discord.triodocs.org)
-
-[Trio documentation](https://triodocs.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://facebook.triodocs.org)
+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).
 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?
 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.
 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.
 If you want to contribute to the development of Trio, please reach out on Discord or Facebook.

+ 15 - 3
Trio.xcodeproj/project.pbxproj

@@ -247,6 +247,7 @@
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
 		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 */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		3BCA5F7C2DC7B16400A7EAC7 /* pumphistory-with-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */; };
 		3BCA5F7C2DC7B16400A7EAC7 /* pumphistory-with-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */; };
@@ -615,6 +616,7 @@
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD53FB2DAA903100F940A6 /* OverviewStepView.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 */; };
 		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 */; };
 		DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */; };
 		DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE790E2D6F97F7000A4D7A /* SubmodulesView.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 */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -625,6 +627,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
 		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 */; };
 		DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */; };
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
@@ -1056,6 +1059,7 @@
 		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-with-external.json"; sourceTree = "<group>"; };
 		3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-with-external.json"; sourceTree = "<group>"; };
@@ -1430,6 +1434,7 @@
 		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
@@ -1440,6 +1445,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
@@ -2382,6 +2388,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
 				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
 				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
 				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
@@ -2442,13 +2449,14 @@
 		38A504F625DDA0E200C5B9E8 /* Extensions */ = {
 		38A504F625DDA0E200C5B9E8 /* Extensions */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */,
+				3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */,
 				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
 				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
-				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
-				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
 				CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */,
 				CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */,
 				CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */,
 				CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */,
-				CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */,
+				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
+				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 			);
 			);
 			path = Extensions;
 			path = Extensions;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2557,6 +2565,7 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
@@ -4148,6 +4157,7 @@
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
+				DDD5889D2DDDC9A900C8848D /* TimeAgoFormatter.swift in Sources */,
 				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
@@ -4535,6 +4545,7 @@
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
+				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
@@ -4632,6 +4643,7 @@
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
+				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,
 				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,

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

@@ -1279,7 +1279,7 @@ extension BaseAPSManager: PumpManagerStatusObserver {
                 guard self.privateContext.hasChanges else { return }
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
                 try self.privateContext.save()
             } catch {
             } 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
         // TODO: - remove this after ensuring that NS still gets the same infos from Core Data

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

@@ -176,7 +176,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                             try self.privateContext.save()
                             try self.privateContext.save()
 
 
                         } catch {
                         } catch {
-                            print("Failed to delete OpenAPS_Battery entries: \(error.localizedDescription)")
+                            debug(.deviceManager, "Failed to delete OpenAPS_Battery entries: \(error)")
                         }
                         }
                     }
                     }
                 }
                 }

+ 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)
+    }
+}

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

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

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

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

+ 18 - 10
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -27,6 +27,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
 
     private let updateSubject = PassthroughSubject<Void, Never>()
     private let updateSubject = PassthroughSubject<Void, Never>()
 
 
+    private let settingsProvider = PickerSettingsProvider.shared
+
     var updatePublisher: AnyPublisher<Void, Never> {
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
         updateSubject.eraseToAnyPublisher()
     }
     }
@@ -111,7 +113,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
 
      - Returns: The computed duration in hours.
      - Returns: The computed duration in hours.
      */
      */
-    private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
+    private func calculateComputedDuration(fpus: Decimal, timeCap: Decimal) -> Decimal {
         switch fpus {
         switch fpus {
         case ..<2:
         case ..<2:
             return 3
             return 3
@@ -145,22 +147,25 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         createdAt: Date,
         createdAt: Date,
         actualDate: Date?
         actualDate: Date?
     ) -> ([CarbsEntry], Decimal) {
     ) -> ([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 kcal = protein * 4 + fat * 9
         let carbEquivalents = (kcal / 10) * adjustment
         let carbEquivalents = (kcal / 10) * adjustment
         let fpus = carbEquivalents / 10
         let fpus = carbEquivalents / 10
         var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
         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 {
         if carbEquivalentSize < 1.0 {
             carbEquivalentSize = 1.0
             carbEquivalentSize = 1.0
-            computedDuration = Int(carbEquivalents / carbEquivalentSize)
+            computedDuration = min(carbEquivalents / carbEquivalentSize, timeCap)
         }
         }
 
 
         let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
         let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
@@ -172,9 +177,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var futureCarbArray = [CarbsEntry]()
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
         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 {
         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
             firstIndex = false
 
 
             let eachCarbEntry = CarbsEntry(
             let eachCarbEntry = CarbsEntry(

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

@@ -287,7 +287,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 let results = try self.context.fetch(fr)
                 let results = try self.context.fetch(fr)
                 date = results.first?.date
                 date = results.first?.date
             } catch let error as NSError {
             } catch let error as NSError {
-                print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
+                debug(.storage, "Fetch error: \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
             }
             }
         }
         }
 
 

+ 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))
+    }
+}

Разлика између датотеке није приказан због своје велике величине
+ 14263 - 698
Trio/Sources/Localizations/Main/Localizable.xcstrings


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

@@ -40,6 +40,29 @@ func info(
     }.perform()
     }.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(
 func warning(
     _ category: Logger.Category,
     _ category: Logger.Category,
     _ message: String,
     _ message: String,
@@ -246,11 +269,22 @@ final class Logger {
         function: String = #function,
         function: String = #function,
         line: UInt = #line
         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)"
         let printedMessage = "INFO: \(message)"
         os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, printedMessage)
         os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, printedMessage)
         reporter.log(category.name, printedMessage, file: file, function: function, line: line)
         reporter.log(category.name, printedMessage, file: file, function: function, line: line)
 
 
-        showAlert(message, type: type)
+        showAlert(notificationText, type: type)
     }
     }
 
 
     func warning(
     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 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 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 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 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)
     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 showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
     var useFPUconversion: Bool = true
     var individualAdjustmentFactor: Decimal = 0.5
     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 useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
     var smoothGlucose: Bool = false
     var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var high: Decimal = 180
     var low: Decimal = 70
     var low: Decimal = 70
-    var hours: Int = 6
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var yGridLines: Bool = true
@@ -167,15 +166,15 @@ extension TrioSettings: Decodable {
             settings.overrideFactor = overrideFactor
             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
             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
             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
             settings.delay = delay
         }
         }
 
 
@@ -237,10 +236,6 @@ extension TrioSettings: Decodable {
             settings.high = high
             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) {
         if let glucoseColorScheme = try? container.decode(GlucoseColorScheme.self, forKey: .glucoseColorScheme) {
             settings.glucoseColorScheme = glucoseColorScheme
             settings.glucoseColorScheme = glucoseColorScheme
         }
         }

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

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

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

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

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

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

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

@@ -81,17 +81,7 @@ struct CurrentGlucoseView: View {
                         }
                         }
                     }
                     }
                     HStack {
                     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 {
                         Group {
                             Text(minutesAgoString)
                             Text(minutesAgoString)
                             Text(delta)
                             Text(delta)

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

@@ -57,15 +57,11 @@ struct LoopView: View {
     }
     }
 
 
     private var timeString: String {
     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 {
         if minutesAgo > 1440 {
             return "--"
             return "--"
-        } else if minutesAgo <= 1 {
-            return "<" + "\u{00A0}" + "1" + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         } else {
         } 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
         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 {
     var body: some View {
         if let pumpStatusHighlightMessage = pumpStatusHighlightMessage { // display message instead pump info
         if let pumpStatusHighlightMessage = pumpStatusHighlightMessage { // display message instead pump info
             VStack(alignment: .center) {
             VStack(alignment: .center) {
@@ -80,7 +97,7 @@ struct PumpView: View {
 
 
                 if let date = expiresAtDate {
                 if let date = expiresAtDate {
                     HStack {
                     HStack {
-                        Image(systemName: "stopwatch.fill")
+                        Image(systemName: hourglassIcon)
                             .font(.callout)
                             .font(.callout)
                             .foregroundStyle(timerColor)
                             .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? {
         var overrideString: String? {

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

@@ -16,36 +16,25 @@ extension MealSettings {
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
 
 
-            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             subscribeSetting(\.maxCarbs, on: $maxCarbs) { maxCarbs = $0 }
             subscribeSetting(\.maxCarbs, on: $maxCarbs) { maxCarbs = $0 }
             subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
             subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
             subscribeSetting(\.maxProtein, on: $maxProtein) { maxProtein = $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 }
             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 }
         }
         }
     }
     }
 }
 }

+ 47 - 14
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -61,16 +61,12 @@ extension Onboarding {
                 return false
                 return false
             }
             }
 
 
-            debug(.default, "Checking for fresh install in \(documentsURL.path)...")
-
             let expectedLogsFolder = "logs"
             let expectedLogsFolder = "logs"
             let expectedPreferencesFile = OpenAPS.Settings.preferences
             let expectedPreferencesFile = OpenAPS.Settings.preferences
 
 
             do {
             do {
                 let contents = try fileManager.contentsOfDirectory(atPath: documentsURL.path)
                 let contents = try fileManager.contentsOfDirectory(atPath: documentsURL.path)
 
 
-                debug(.default, "Found \(contents) in \(documentsURL.path)...")
-
                 // Expect exactly 2 entries: "logs" and the preferences file
                 // Expect exactly 2 entries: "logs" and the preferences file
                 guard contents.count == 2 else {
                 guard contents.count == 2 else {
                     debug(.default, "Trio install is not fresh; returning user.")
                     debug(.default, "Trio install is not fresh; returning user.")
@@ -81,8 +77,6 @@ extension Onboarding {
                 let expectedSet = Set([expectedLogsFolder, expectedPreferencesFile])
                 let expectedSet = Set([expectedLogsFolder, expectedPreferencesFile])
                 let actualSet = Set(contents)
                 let actualSet = Set(contents)
 
 
-                debug(.default, "Expected: \(expectedSet), Actual: \(actualSet)")
-
                 let isFreshInstall = expectedSet == actualSet
                 let isFreshInstall = expectedSet == actualSet
                 debug(.default, "Trio install is fresh; new user.")
                 debug(.default, "Trio install is fresh; new user.")
 
 
@@ -258,10 +252,12 @@ extension Onboarding {
             isConnectingToNS = false
             isConnectingToNS = false
             isValidNightscoutURL = 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
         // MARK: - Helpers
@@ -417,15 +413,16 @@ extension Onboarding {
         ///   - `units` from app settings.
         ///   - `units` from app settings.
         func fetchExistingDeliveryLimtisFromFile() {
         func fetchExistingDeliveryLimtisFromFile() {
             let pumpSettingsFromFile = provider.pumpSettingsFromFile
             let pumpSettingsFromFile = provider.pumpSettingsFromFile
+            let providedSettings = settingsProvider.settings
 
 
             if let pumpSettingsFromFile = pumpSettingsFromFile {
             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
             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
             minimumSafetyThreshold = preferences.threshold_setting
         }
         }
 
 
@@ -702,6 +699,30 @@ extension Onboarding {
         func applyToSettings() {
         func applyToSettings() {
             var settingsCopy = settingsManager.settings
             var settingsCopy = settingsManager.settings
             settingsCopy.units = units
             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
             settingsManager.settings = settingsCopy
         }
         }
 
 
@@ -744,6 +765,18 @@ extension Onboarding {
                 preferences.suspendZerosIOB = true
                 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
             settingsManager.preferences = preferences
         }
         }
 
 

+ 91 - 8
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -1,6 +1,7 @@
 import Combine
 import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import LocalAuthentication
 import LoopKit
 import LoopKit
 import Observation
 import Observation
 import SwiftUI
 import SwiftUI
@@ -471,6 +472,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 {
         func addPumpInsulin() async {
             guard amount > 0 else {
             guard amount > 0 else {
                 showModal(for: nil)
                 showModal(for: nil)
@@ -487,15 +573,14 @@ extension Treatments {
                         self.isAwaitingDeterminationResult = true
                         self.isAwaitingDeterminationResult = true
                     }
                     }
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
-                } else {
-                    print("authentication failed")
                 }
                 }
             } catch {
             } catch {
-                print("authentication error for pump bolus: \(error.localizedDescription)")
+                debug(.bolusState, "Authentication error for pump bolus: \(error)")
+
                 await MainActor.run {
                 await MainActor.run {
                     self.isAwaitingDeterminationResult = false
                     self.isAwaitingDeterminationResult = false
                     self.showDeterminationFailureAlert = true
                     self.showDeterminationFailureAlert = true
-                    self.determinationFailureMessage = error.localizedDescription
+                    self.determinationFailureMessage = parseAuthenticationError(from: error)
                 }
                 }
             }
             }
         }
         }
@@ -523,15 +608,13 @@ extension Treatments {
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     // perform determine basal sync
                     // perform determine basal sync
                     try await apsManager.determineBasalSync()
                     try await apsManager.determineBasalSync()
-                } else {
-                    print("authentication failed")
                 }
                 }
             } catch {
             } catch {
-                print("authentication error for external insulin: \(error.localizedDescription)")
+                debug(.bolusState, "authentication error for external insulin: \(error)")
                 await MainActor.run {
                 await MainActor.run {
                     self.isAwaitingDeterminationResult = false
                     self.isAwaitingDeterminationResult = false
                     self.showDeterminationFailureAlert = true
                     self.showDeterminationFailureAlert = true
-                    self.determinationFailureMessage = error.localizedDescription
+                    self.determinationFailureMessage = parseAuthenticationError(from: error)
                 }
                 }
             }
             }
         }
         }

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

@@ -408,12 +408,12 @@ extension Treatments {
             }) {
             }) {
                 MealPresetView(state: state)
                 MealPresetView(state: state)
             }
             }
-            .alert("Determination Failed", isPresented: $state.showDeterminationFailureAlert) {
+            .alert("Error while processing Treatment", isPresented: $state.showDeterminationFailureAlert) {
                 Button("OK", role: .cancel) {
                 Button("OK", role: .cancel) {
                     state.hideModal()
                     state.hideModal()
                 }
                 }
             } message: {
             } message: {
-                Text("Failed to update COB/IOB: \(state.determinationFailureMessage)")
+                Text("\(state.determinationFailureMessage)")
             }
             }
         }
         }
 
 

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

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

+ 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"))
+            """
+        )
+    }
+}