Просмотр исходного кода

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

Marvin Polscheit 1 год назад
Родитель
Сommit
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_VERSION = 0.5.0
-APP_DEV_VERSION = 0.5.0.3
+APP_DEV_VERSION = 0.5.0.15
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 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:
 
-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,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:
 
 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.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).
 
-## 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.

+ 15 - 3
Trio.xcodeproj/project.pbxproj

@@ -247,6 +247,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 */; };
 		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 */; };
 		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 */; };
@@ -625,6 +627,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 */; };
@@ -1056,6 +1059,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>"; };
 		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>"; };
 		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>"; };
@@ -1440,6 +1445,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>"; };
@@ -2382,6 +2388,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
 				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
@@ -2442,13 +2449,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>";
@@ -2557,6 +2565,7 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
@@ -4148,6 +4157,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 */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
@@ -4535,6 +4545,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 */,
@@ -4632,6 +4643,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 */,

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

@@ -1279,7 +1279,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

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

+ 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 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 }
@@ -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/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 {

+ 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 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(

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

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

+ 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()
 }
 
+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
@@ -167,15 +166,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
         }
 
@@ -237,10 +236,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
         }

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

+ 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, String(describing: error))
+                        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? {

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

+ 47 - 14
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.")
 
@@ -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
         }
 

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

@@ -1,6 +1,7 @@
 import Combine
 import CoreData
 import Foundation
+import LocalAuthentication
 import LoopKit
 import Observation
 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 {
             guard amount > 0 else {
                 showModal(for: nil)
@@ -487,15 +573,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)
                 }
             }
         }
@@ -523,15 +608,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)
                 }
             }
         }

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

@@ -408,12 +408,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)")
             }
         }
 

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

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