Browse Source

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	LoopFollow/Resources/Localizable.xcstrings
q5078620 3 ngày trước cách đây
mục cha
commit
feb783f329
36 tập tin đã thay đổi với 6661 bổ sung4904 xóa
  1. 7 7
      LoopFollow/Controllers/BackgroundAlertManager.swift
  2. 8 8
      LoopFollow/Controllers/Graphs.swift
  3. 2 2
      LoopFollow/Controllers/MainViewController+updateStats.swift
  4. 2 2
      LoopFollow/Controllers/Nightscout/BGData.swift
  5. 1 1
      LoopFollow/Controllers/Nightscout/DeviceStatus.swift
  6. 2 2
      LoopFollow/Controllers/StatsDisplayModel.swift
  7. 4 4
      LoopFollow/Controllers/StatsDisplayView.swift
  8. 1 1
      LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift
  9. 20 20
      LoopFollow/InfoTable/InfoType.swift
  10. 2 2
      LoopFollow/LiveActivitySettingsView.swift
  11. 1 1
      LoopFollow/Nightscout/NightscoutSettingsView.swift
  12. 13 13
      LoopFollow/Nightscout/NightscoutSettingsViewModel.swift
  13. 9 9
      LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift
  14. 9 9
      LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift
  15. 8 8
      LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift
  16. 1 1
      LoopFollow/Remote/LoopAPNS/OverridePresetData.swift
  17. 5 5
      LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift
  18. 5 5
      LoopFollow/Remote/Settings/RemoteCommandSettings.swift
  19. 7 7
      LoopFollow/Remote/Settings/RemoteSettingsView.swift
  20. 7 7
      LoopFollow/Remote/TRC/BolusView.swift
  21. 11 11
      LoopFollow/Remote/TRC/MealView.swift
  22. 7 7
      LoopFollow/Remote/TRC/OverrideView.swift
  23. 6 6
      LoopFollow/Remote/TRC/TempTargetView.swift
  24. 1 1
      LoopFollow/Remote/TRC/TrioRemoteControlView.swift
  25. 6300 4727
      LoopFollow/Resources/Localizable.xcstrings
  26. 2 2
      LoopFollow/Snoozer/SnoozerView.swift
  27. 1 1
      LoopFollow/Stats/AGP/AGPGraphView.swift
  28. 1 1
      LoopFollow/Stats/AGP/AGPView.swift
  29. 6 6
      LoopFollow/Stats/AggregatedStatsView.swift
  30. 4 4
      LoopFollow/Stats/DataAvailabilityCalculator.swift
  31. 2 2
      LoopFollow/Stats/DateRangePicker.swift
  32. 6 6
      LoopFollow/Stats/GRI/GRIRiskGridView.swift
  33. 5 5
      LoopFollow/Stats/GRI/GRIView.swift
  34. 3 3
      LoopFollow/Stats/TIR/TIRGraphView.swift
  35. 8 8
      LoopFollow/Stats/TIR/TIRView.swift
  36. 184 0
      Scripts/gen_zh_catalog.py

+ 7 - 7
LoopFollow/Controllers/BackgroundAlertManager.swift

@@ -40,7 +40,7 @@ class BackgroundAlertManager {
     private var isAlertScheduled: Bool = false
 
     /// Title prefix for all background refresh notifications.
-    private let notificationTitlePrefix = "LoopFollow Background Refresh"
+    private let notificationTitlePrefix = String(localized: "LoopFollow Background Refresh")
 
     /// Timestamp of the last scheduled background alert.
     private var lastScheduleDate: Date?
@@ -84,22 +84,22 @@ class BackgroundAlertManager {
                 identifier: BackgroundAlertIdentifier.sixMin.rawValue,
                 timeInterval: BackgroundAlertDuration.sixMinutes.rawValue,
                 body: isBluetoothActive
-                    ? "App inactive for 6 minutes. Verify Bluetooth connectivity."
-                    : "App inactive for 6 minutes. Open to resume."
+                    ? String(localized: "App inactive for 6 minutes. Verify Bluetooth connectivity.")
+                    : String(localized: "App inactive for 6 minutes. Open to resume.")
             ),
             BackgroundAlert(
                 identifier: BackgroundAlertIdentifier.twelveMin.rawValue,
                 timeInterval: BackgroundAlertDuration.twelveMinutes.rawValue,
                 body: isBluetoothActive
-                    ? "App inactive for 12 minutes. Verify Bluetooth connectivity."
-                    : "App inactive for 12 minutes. Open to resume."
+                    ? String(localized: "App inactive for 12 minutes. Verify Bluetooth connectivity.")
+                    : String(localized: "App inactive for 12 minutes. Open to resume.")
             ),
             BackgroundAlert(
                 identifier: BackgroundAlertIdentifier.eighteenMin.rawValue,
                 timeInterval: BackgroundAlertDuration.eighteenMinutes.rawValue,
                 body: isBluetoothActive
-                    ? "App inactive for 18 minutes. Verify Bluetooth connectivity."
-                    : "App inactive for 18 minutes. Open to resume."
+                    ? String(localized: "App inactive for 18 minutes. Verify Bluetooth connectivity.")
+                    : String(localized: "App inactive for 18 minutes. Open to resume.")
             ),
         ]
 

+ 8 - 8
LoopFollow/Controllers/Graphs.swift

@@ -1317,7 +1317,7 @@ extension MainViewController {
             let graphHours = 24 * Storage.shared.downloadDays.value
             if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
 
-            let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Suspend Pump", time: thisData[i].date))
+            let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: String(localized: "Suspend Pump"), time: thisData[i].date))
             BGChart.data?.dataSets[dataIndex].addEntry(value)
             if Storage.shared.smallGraphTreatments.value {
                 BGChartFull.data?.dataSets[dataIndex].addEntry(value)
@@ -1344,7 +1344,7 @@ extension MainViewController {
             let graphHours = 24 * Storage.shared.downloadDays.value
             if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
 
-            let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Resume Pump", time: thisData[i].date))
+            let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: String(localized: "Resume Pump"), time: thisData[i].date))
             BGChart.data?.dataSets[dataIndex].addEntry(value)
             if Storage.shared.smallGraphTreatments.value {
                 BGChartFull.data?.dataSets[dataIndex].addEntry(value)
@@ -1371,7 +1371,7 @@ extension MainViewController {
             let graphHours = 24 * Storage.shared.downloadDays.value
             if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
 
-            let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Start Sensor", time: thisData[i].date))
+            let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: String(localized: "Start Sensor"), time: thisData[i].date))
             BGChart.data?.dataSets[dataIndex].addEntry(value)
             if Storage.shared.smallGraphTreatments.value {
                 BGChartFull.data?.dataSets[dataIndex].addEntry(value)
@@ -1759,7 +1759,7 @@ extension MainViewController {
                 labelText += String(thisItem.correctionRange[0]) + "-" + String(thisItem.correctionRange[1])
             }
             if thisItem.enteredBy.count > 0 {
-                labelText += "\r\nEntered By: " + thisItem.enteredBy
+                labelText += "\r\n" + String(localized: "Entered By: ") + thisItem.enteredBy
             }
 
             // Start Dot
@@ -1908,17 +1908,17 @@ extension MainViewController {
 
             // Check for bolus entry
             if let bolusAmount = json["bolus-entry"] as? Double {
-                actionParts.append("Bolus: \(bolusAmount) U")
+                actionParts.append(String(localized: "Bolus:") + " \(bolusAmount) U")
             }
 
             // Check for carbs entry
             if let carbsAmount = json["carbs-entry"] as? Double {
-                actionParts.append("Carbs: \(carbsAmount) g")
+                actionParts.append(String(localized: "Carbs:") + " \(carbsAmount) g")
             }
 
             // Check for absorption time (relevant for carbs)
             if let absorptionTime = json["absorption-time"] as? Double {
-                actionParts.append("Absorption: \(absorptionTime) hrs")
+                actionParts.append(String(localized: "Absorption:") + " \(absorptionTime) " + String(localized: "hrs"))
             }
 
             // Check for OTP (password)
@@ -1928,7 +1928,7 @@ extension MainViewController {
 
             // Check for sender
             if let enteredBy = json["entered-by"] as? String {
-                actionParts.append("From: \(enteredBy)")
+                actionParts.append(String(localized: "From:") + " \(enteredBy)")
             }
 
             // Combine action parts

+ 2 - 2
LoopFollow/Controllers/MainViewController+updateStats.swift

@@ -25,7 +25,7 @@ extension MainViewController {
             statsDisplayModel.highPercent = String(format: "%.1f%%", stats.percentHigh)
             statsDisplayModel.avgBG = Localizer.toDisplayUnits(String(format: "%.0f", stats.avgBG))
 
-            statsDisplayModel.estA1CTitle = UnitSettingsStore.shared.glycemicMetricMode == .gmi ? "GMI:" : "Est. A1C:"
+            statsDisplayModel.estA1CTitle = UnitSettingsStore.shared.glycemicMetricMode == .gmi ? "GMI:" : String(localized: "Est. A1C:")
             if UnitSettingsStore.shared.glycemicOutputUnit == .mmolMol {
                 statsDisplayModel.estA1C = String(format: "%.0f", stats.a1C)
             } else {
@@ -33,7 +33,7 @@ extension MainViewController {
             }
 
             if UnitSettingsStore.shared.variabilityMetricMode == .stdDeviation {
-                statsDisplayModel.stdDevTitle = "Std Dev:"
+                statsDisplayModel.stdDevTitle = String(localized: "Std Dev:")
                 if UnitSettingsStore.shared.glucoseUnit == .mgdL {
                     statsDisplayModel.stdDev = String(format: "%.0f", stats.stdDev)
                 } else {

+ 2 - 2
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -287,9 +287,9 @@ extension MainViewController {
             self.updateBGTextAppearance()
 
             if latestBG <= globalVariables.minDisplayGlucose {
-                Observable.shared.bgText.value = "LOW"
+                Observable.shared.bgText.value = String(localized: "LOW")
             } else if latestBG >= globalVariables.maxDisplayGlucose {
-                Observable.shared.bgText.value = "HIGH"
+                Observable.shared.bgText.value = String(localized: "HIGH")
             } else {
                 Observable.shared.bgText.value = Localizer.toDisplayUnits(String(latestBG))
             }

+ 1 - 1
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -46,7 +46,7 @@ extension MainViewController {
             IsNotLooping = true
             Observable.shared.isNotLooping.value = true
 
-            Observable.shared.loopStatusText.value = "⚠️ Not Looping!"
+            Observable.shared.loopStatusText.value = String(localized: "⚠️ Not Looping!")
             Observable.shared.loopStatusColor.value = .yellow
             #if !targetEnvironment(macCatalyst)
                 LiveActivityManager.shared.refreshFromCurrentState(reason: "notLooping")

+ 2 - 2
LoopFollow/Controllers/StatsDisplayModel.swift

@@ -9,9 +9,9 @@ class StatsDisplayModel: ObservableObject {
     @Published var highPercent: String = ""
     @Published var avgBG: String = ""
     @Published var estA1C: String = ""
-    @Published var estA1CTitle: String = "Est A1C:"
+    @Published var estA1CTitle: String = String(localized: "Est A1C:")
     @Published var stdDev: String = ""
-    @Published var stdDevTitle: String = "Std Dev:"
+    @Published var stdDevTitle: String = String(localized: "Std Dev:")
     @Published var pieLow: Double = 0
     @Published var pieRange: Double = 0
     @Published var pieHigh: Double = 0

+ 4 - 4
LoopFollow/Controllers/StatsDisplayView.swift

@@ -19,12 +19,12 @@ struct StatsDisplayView: View {
 
             VStack(spacing: 10) {
                 HStack {
-                    statColumn(title: "Low:", value: model.lowPercent)
-                    statColumn(title: "In Range:", value: model.inRangePercent)
-                    statColumn(title: "High:", value: model.highPercent)
+                    statColumn(title: String(localized: "Low:"), value: model.lowPercent)
+                    statColumn(title: String(localized: "In Range:"), value: model.inRangePercent)
+                    statColumn(title: String(localized: "High:"), value: model.highPercent)
                 }
                 HStack {
-                    statColumn(title: "Avg BG:", value: model.avgBG)
+                    statColumn(title: String(localized: "Avg BG:"), value: model.avgBG)
                     statColumn(title: model.estA1CTitle, value: model.estA1C)
                     statColumn(title: model.stdDevTitle, value: model.stdDev)
                 }

+ 1 - 1
LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift

@@ -25,7 +25,7 @@ class InfoDisplaySettingsViewModel: ObservableObject {
 
     func getName(for index: Int) -> String {
         guard let infoType = InfoType(rawValue: index) else {
-            return "Unknown"
+            return String(localized: "Unknown")
         }
         return infoType.name
     }

+ 20 - 20
LoopFollow/InfoTable/InfoType.swift

@@ -8,26 +8,26 @@ enum InfoType: Int, CaseIterable {
 
     var name: String {
         switch self {
-        case .iob: return "IOB"
-        case .cob: return "COB"
-        case .basal: return "Basal"
-        case .override: return "Override"
-        case .battery: return "Battery"
-        case .pump: return "Pump"
-        case .pumpBattery: return "Pump Battery"
-        case .sage: return "SAGE"
-        case .cage: return "CAGE"
-        case .recBolus: return "Rec. Bolus"
-        case .minMax: return "Min/Max"
-        case .carbsToday: return "Carbs today"
-        case .autosens: return "Autosens"
-        case .profile: return "Profile"
-        case .target: return "Target"
-        case .isf: return "ISF"
-        case .carbRatio: return "CR"
-        case .updated: return "Updated"
-        case .tdd: return "TDD"
-        case .iage: return "IAGE"
+        case .iob: return String(localized: "IOB")
+        case .cob: return String(localized: "COB")
+        case .basal: return String(localized: "Basal")
+        case .override: return String(localized: "Override")
+        case .battery: return String(localized: "Battery")
+        case .pump: return String(localized: "Pump")
+        case .pumpBattery: return String(localized: "Pump Battery")
+        case .sage: return String(localized: "SAGE")
+        case .cage: return String(localized: "CAGE")
+        case .recBolus: return String(localized: "Rec. Bolus")
+        case .minMax: return String(localized: "Min/Max")
+        case .carbsToday: return String(localized: "Carbs today")
+        case .autosens: return String(localized: "Autosens")
+        case .profile: return String(localized: "Profile")
+        case .target: return String(localized: "Target")
+        case .isf: return String(localized: "ISF")
+        case .carbRatio: return String(localized: "CR")
+        case .updated: return String(localized: "Updated")
+        case .tdd: return String(localized: "TDD")
+        case .iage: return String(localized: "IAGE")
         }
     }
 

+ 2 - 2
LoopFollow/LiveActivitySettingsView.swift

@@ -13,7 +13,7 @@
         @State private var keyId: String = Storage.shared.lfKeyId.value
         @State private var apnsKey: String = Storage.shared.lfApnsKey.value
 
-        private let slotLabels = ["Top left", "Top right", "Bottom left", "Bottom right"]
+        private let slotLabels = [String(localized: "Top left"), String(localized: "Top right"), String(localized: "Bottom left"), String(localized: "Bottom right")]
 
         private var apnsConfigured: Bool {
             APNsCredentialValidator.isFullyConfigured(keyId: keyId, apnsKey: apnsKey)
@@ -42,7 +42,7 @@
                     }
 
                     Section {
-                        Button(restartConfirmed ? "Live Activity Restarted" : "Restart Live Activity") {
+                        Button(restartConfirmed ? String(localized: "Live Activity Restarted") : String(localized: "Restart Live Activity")) {
                             LiveActivityManager.shared.forceRestart()
                             restartConfirmed = true
                             DispatchQueue.main.asyncAfter(deadline: .now() + 2) {

+ 1 - 1
LoopFollow/Nightscout/NightscoutSettingsView.swift

@@ -53,7 +53,7 @@ struct NightscoutSettingsView: View {
             HStack {
                 Text("Access Token")
                 TogglableSecureInput(
-                    placeholder: "Enter Token",
+                    placeholder: String(localized: "Enter Token"),
                     text: $viewModel.nightscoutToken,
                     style: .singleLine,
                     textContentType: .password

+ 13 - 13
LoopFollow/Nightscout/NightscoutSettingsViewModel.swift

@@ -33,7 +33,7 @@ class NightscoutSettingsViewModel: ObservableObject {
         }
     }
 
-    @Published var nightscoutStatus: String = "Checking..."
+    @Published var nightscoutStatus: String = String(localized: "Checking...")
 
     @Published var webSocketEnabled: Bool = Storage.shared.webSocketEnabled.value {
         didSet {
@@ -47,7 +47,7 @@ class NightscoutSettingsViewModel: ObservableObject {
         }
     }
 
-    @Published var webSocketStatus: String = "Disconnected"
+    @Published var webSocketStatus: String = String(localized: "Disconnected")
 
     var webSocketStatusColor: Color {
         switch NightscoutSocketManager.shared.connectionState {
@@ -84,7 +84,7 @@ class NightscoutSettingsViewModel: ObservableObject {
     private func triggerCheckStatus() {
         checkStatusWorkItem?.cancel()
 
-        nightscoutStatus = "Checking..."
+        nightscoutStatus = String(localized: "Checking...")
 
         checkStatusWorkItem = DispatchWorkItem {
             self.checkStatusSubject.send()
@@ -136,26 +136,26 @@ class NightscoutSettingsViewModel: ObservableObject {
             isConnected = false
             switch error {
             case .invalidURL:
-                nightscoutStatus = "Invalid URL"
+                nightscoutStatus = String(localized: "Invalid URL")
             case .networkError:
-                nightscoutStatus = "Network Error"
+                nightscoutStatus = String(localized: "Network Error")
             case .invalidToken:
-                nightscoutStatus = "Invalid Token"
+                nightscoutStatus = String(localized: "Invalid Token")
             case .tokenRequired:
-                nightscoutStatus = "Token Required"
+                nightscoutStatus = String(localized: "Token Required")
             case .siteNotFound:
-                nightscoutStatus = "Site Not Found"
+                nightscoutStatus = String(localized: "Site Not Found")
             case .unknown:
-                nightscoutStatus = "Unknown Error"
+                nightscoutStatus = String(localized: "Unknown Error")
             case .emptyAddress:
-                nightscoutStatus = "Address Empty"
+                nightscoutStatus = String(localized: "Address Empty")
             }
             NightscoutSocketManager.shared.disconnect()
         } else {
             isConnected = true
             let authStatus: String
             if Storage.shared.nsAdminAuth.value {
-                authStatus = "Admin"
+                authStatus = String(localized: "Admin")
             } else {
                 authStatus = "Read" + (Storage.shared.nsWriteAuth.value ? " & Write" : "")
             }
@@ -184,8 +184,8 @@ class NightscoutSettingsViewModel: ObservableObject {
 
     private func updateWebSocketStatus() {
         switch NightscoutSocketManager.shared.connectionState {
-        case .disconnected: webSocketStatus = "Disconnected"
-        case .connecting: webSocketStatus = "Connecting..."
+        case .disconnected: webSocketStatus = String(localized: "Disconnected")
+        case .connecting: webSocketStatus = String(localized: "Connecting...")
         case .connected: webSocketStatus = "Connected"
         case .authenticated: webSocketStatus = "Connected"
         case .error: webSocketStatus = "Error"

+ 9 - 9
LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift

@@ -99,7 +99,7 @@ struct LoopAPNSBolusView: View {
 
                     Section {
                         HKQuantityInputView(
-                            label: "Insulin Amount",
+                            label: String(localized: "Insulin Amount"),
                             quantity: $insulinAmount,
                             unit: .internationalUnit(),
                             maxLength: 5,
@@ -206,7 +206,7 @@ struct LoopAPNSBolusView: View {
                 // Validate APNS setup
                 let apnsService = LoopAPNSService()
                 if !apnsService.validateSetup() {
-                    alertMessage = "Loop APNS setup is incomplete. Please configure all required fields in settings."
+                    alertMessage = String(localized: "Loop APNS setup is incomplete. Please configure all required fields in settings.")
                     alertType = .error
                     showAlert = true
                 }
@@ -260,7 +260,7 @@ struct LoopAPNSBolusView: View {
                     // Only show warning if calculation is between 5-12 minutes old
                     if minutesSinceCalculation > 5 && minutesSinceCalculation < 12 && !showOldCalculationWarning {
                         showOldCalculationWarning = true
-                        alertMessage = "This recommended bolus was calculated \(minutesSinceCalculation) minutes ago. New treatments may have occurred since then. Proceed with caution."
+                        alertMessage = String(localized: "This recommended bolus was calculated \(minutesSinceCalculation) minutes ago. New treatments may have occurred since then. Proceed with caution.")
                         alertType = .oldCalculationWarning
                         showAlert = true
                     }
@@ -337,7 +337,7 @@ struct LoopAPNSBolusView: View {
 
     private func sendInsulin() {
         guard insulinAmount.doubleValue(for: .internationalUnit()) > 0 else {
-            alertMessage = "Please enter a valid insulin amount"
+            alertMessage = String(localized: "Please enter a valid insulin amount")
             alertType = .error
             showAlert = true
             return
@@ -359,7 +359,7 @@ struct LoopAPNSBolusView: View {
     }
 
     private func authenticateAndSendInsulin() {
-        AuthService.authenticate(reason: "Confirm your identity to send insulin.") { result in
+        AuthService.authenticate(reason: String(localized: "Confirm your identity to send insulin.")) { result in
             DispatchQueue.main.async {
                 switch result {
                 case .success:
@@ -369,7 +369,7 @@ struct LoopAPNSBolusView: View {
                     self.alertType = .error
                     self.showAlert = true
                 case .failed:
-                    self.alertMessage = "Authentication failed"
+                    self.alertMessage = String(localized: "Authentication failed")
                     self.alertType = .error
                     self.showAlert = true
                 case .canceled:
@@ -385,7 +385,7 @@ struct LoopAPNSBolusView: View {
 
         // Extract OTP from QR code URL
         guard let otpCode = TOTPGenerator.extractOTPFromURL(Storage.shared.loopAPNSQrCodeURL.value) else {
-            alertMessage = "Invalid QR code URL. Please re-scan the QR code in settings."
+            alertMessage = String(localized: "Invalid QR code URL. Please re-scan the QR code in settings.")
             alertType = .error
             isLoading = false
             showAlert = true
@@ -409,14 +409,14 @@ struct LoopAPNSBolusView: View {
                     }
                     // Mark TOTP code as used
                     TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
-                    self.alertMessage = "Insulin sent successfully!"
+                    self.alertMessage = String(localized: "Insulin sent successfully!")
                     self.alertType = .success
                     LogManager.shared.log(
                         category: .apns,
                         message: "Insulin sent - Amount: \(insulinAmount.doubleValue(for: .internationalUnit()))U"
                     )
                 } else {
-                    self.alertMessage = errorMessage ?? "Failed to send insulin. Check your Loop APNS configuration."
+                    self.alertMessage = errorMessage ?? String(localized: "Failed to send insulin. Check your Loop APNS configuration.")
                     self.alertType = .error
                     LogManager.shared.log(
                         category: .apns,

+ 9 - 9
LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift

@@ -129,7 +129,7 @@ struct LoopAPNSCarbsView: View {
 
                     Section {
                         HKQuantityInputView(
-                            label: "Carbs Amount",
+                            label: String(localized: "Carbs Amount"),
                             quantity: $carbsAmount,
                             unit: .gram(),
                             maxLength: 4,
@@ -405,7 +405,7 @@ struct LoopAPNSCarbsView: View {
                 // Validate APNS setup
                 let apnsService = LoopAPNSService()
                 if !apnsService.validateSetup() {
-                    alertMessage = "Loop APNS setup is incomplete. Please configure all required fields in settings."
+                    alertMessage = String(localized: "Loop APNS setup is incomplete. Please configure all required fields in settings.")
                     alertType = .error
                     showAlert = true
                 }
@@ -486,7 +486,7 @@ struct LoopAPNSCarbsView: View {
 
     private func sendCarbs() {
         guard carbsAmount.doubleValue(for: .gram()) > 0 else {
-            alertMessage = "Please enter a valid carb amount"
+            alertMessage = String(localized: "Please enter a valid carb amount")
             alertType = .error
             showAlert = true
             return
@@ -497,7 +497,7 @@ struct LoopAPNSCarbsView: View {
         let carbsValue = carbsAmount.doubleValue(for: .gram())
 
         if carbsValue > maxCarbs {
-            alertMessage = "Carbs amount (\(Int(carbsValue))g) exceeds the maximum allowed (\(Int(maxCarbs))g). Please reduce the amount."
+            alertMessage = String(localized: "Carbs amount (\(Int(carbsValue))g) exceeds the maximum allowed (\(Int(maxCarbs))g). Please reduce the amount.")
             alertType = .error
             showAlert = true
             return
@@ -509,14 +509,14 @@ struct LoopAPNSCarbsView: View {
         let latestAcceptedDate = now.addingTimeInterval(60 * 60 * Double(maxFutureHours))
 
         if consumedDate < oldestAcceptedDate {
-            alertMessage = "Time must be within the prior \(maxPastHours) hours"
+            alertMessage = String(localized: "Time must be within the prior \(maxPastHours) hours")
             alertType = .error
             showAlert = true
             return
         }
 
         if consumedDate > latestAcceptedDate {
-            alertMessage = "Time must be within the next \(maxFutureHours) hour"
+            alertMessage = String(localized: "Time must be within the next \(maxFutureHours) hour")
             alertType = .error
             showAlert = true
             return
@@ -531,7 +531,7 @@ struct LoopAPNSCarbsView: View {
 
         // Extract OTP from QR code URL
         guard let otpCode = TOTPGenerator.extractOTPFromURL(Storage.shared.loopAPNSQrCodeURL.value) else {
-            alertMessage = "Invalid QR code URL. Please re-scan the QR code in settings."
+            alertMessage = String(localized: "Invalid QR code URL. Please re-scan the QR code in settings.")
             alertType = .error
             isLoading = false
             showAlert = true
@@ -573,14 +573,14 @@ struct LoopAPNSCarbsView: View {
                     TOTPService.shared.markTOTPAsUsed(qrCodeURL: Storage.shared.loopAPNSQrCodeURL.value)
                     let timeFormatter = DateFormatter()
                     timeFormatter.timeStyle = .short
-                    self.alertMessage = "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!"
+                    self.alertMessage = String(localized: "Carbs sent successfully for \(timeFormatter.string(from: adjustedConsumedDate))!")
                     self.alertType = .success
                     LogManager.shared.log(
                         category: .apns,
                         message: "Carbs sent - Amount: \(carbsAmount.doubleValue(for: .gram()))g, Absorption: \(absorptionConfirmationText)h, Time: \(adjustedConsumedDate)"
                     )
                 } else {
-                    self.alertMessage = errorMessage ?? "Failed to send carbs. Check your Loop APNS configuration."
+                    self.alertMessage = errorMessage ?? String(localized: "Failed to send carbs. Check your Loop APNS configuration.")
                     self.alertType = .error
                     LogManager.shared.log(
                         category: .apns,

+ 8 - 8
LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift

@@ -36,25 +36,25 @@ class LoopAPNSService {
         var errorDescription: String? {
             switch self {
             case .invalidConfiguration:
-                return "Loop APNS Configuration not valid"
+                return String(localized: "Loop APNS Configuration not valid")
             case .jwtError:
-                return "Failed generating JWT token, check APNS Key ID, APNS Key and Team ID"
+                return String(localized: "Failed generating JWT token, check APNS Key ID, APNS Key and Team ID")
             case .networkError:
-                return "Network error occurred"
+                return String(localized: "Network error occurred")
             case .invalidResponse:
-                return "Invalid response from server"
+                return String(localized: "Invalid response from server")
             case .noDeviceToken:
-                return "No device token found in profile"
+                return String(localized: "No device token found in profile")
             case .noBundleIdentifier:
                 return "No bundle identifier found in profile"
             case .unauthorized:
-                return "Unauthorized - check your API secret"
+                return String(localized: "Unauthorized - check your API secret")
             case .deviceTokenNotConfigured:
-                return "Device token not configured"
+                return String(localized: "Device token not configured")
             case .bundleIdentifierNotConfigured:
                 return "Bundle identifier not configured"
             case .rateLimited:
-                return "Too many requests - please wait a few minutes before trying again"
+                return String(localized: "Too many requests - please wait a few minutes before trying again")
             }
         }
     }

+ 1 - 1
LoopFollow/Remote/LoopAPNS/OverridePresetData.swift

@@ -49,7 +49,7 @@ struct OverridePresetData: Codable, Identifiable {
 
     var durationDescription: String {
         if duration == 0 {
-            return "Indefinite"
+            return String(localized: "Indefinite")
         } else {
             let hours = Int(duration / 3600)
             let minutes = Int((duration.truncatingRemainder(dividingBy: 3600)) / 60)

+ 5 - 5
LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift

@@ -126,7 +126,7 @@ struct OverridePresetsView: View {
                 case .statusFailure:
                     return Alert(
                         title: Text("Error"),
-                        message: Text(viewModel.statusMessage ?? "An error occurred."),
+                        message: Text(viewModel.statusMessage ?? String(localized: "An error occurred.")),
                         dismissButton: .default(Text("OK"))
                     )
                 case .none:
@@ -428,7 +428,7 @@ class OverridePresetsViewModel: ObservableObject {
             try await sendCancelOverrideNotification()
             await MainActor.run {
                 self.isActivating = false
-                self.statusMessage = "Active override cancelled successfully."
+                self.statusMessage = String(localized: "Active override cancelled successfully.")
                 self.alertType = .statusSuccess
                 self.showAlert = true
             }
@@ -506,7 +506,7 @@ struct OverridePreset {
 
     var durationDescription: String {
         if duration == 0 {
-            return "Indefinite"
+            return String(localized: "Indefinite")
         } else {
             let hours = Int(duration) / 3600
             let minutes = Int(duration) % 3600 / 60
@@ -527,9 +527,9 @@ enum OverrideError: LocalizedError {
     var errorDescription: String? {
         switch self {
         case .nightscoutNotConfigured:
-            return "Nightscout URL and token not configured in settings"
+            return String(localized: "Nightscout URL and token not configured in settings")
         case .invalidResponse:
-            return "Invalid response from server"
+            return String(localized: "Invalid response from server")
         case let .serverError(code):
             return "Server error: \(code)"
         }

+ 5 - 5
LoopFollow/Remote/Settings/RemoteCommandSettings.swift

@@ -174,16 +174,16 @@ struct RemoteCommandSettings: Codable {
         // If current user doesn't have URL but scanned settings do, prompt to set it
         if !hasCurrentURL, hasScannedURL {
             shouldPromptForURL = true
-            message = "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?"
+            message = String(localized: "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?")
         }
 
         // If current user doesn't have token but scanned settings do, prompt to set it
         if !hasCurrentToken, hasScannedToken {
             shouldPromptForToken = true
             if !message.isEmpty {
-                message += "\n\nThe scanned settings also include a token. Would you like to set this as your access token?"
+                message += "\n\n" + String(localized: "The scanned settings also include a token. Would you like to set this as your access token?")
             } else {
-                message = "The scanned settings include a token. Would you like to set this as your access token?"
+                message = String(localized: "The scanned settings include a token. Would you like to set this as your access token?")
             }
         }
 
@@ -197,9 +197,9 @@ struct RemoteCommandSettings: Codable {
         if hasCurrentToken, hasScannedToken, currentToken != token {
             shouldPromptForToken = true
             if !message.isEmpty {
-                message += "\n\nThe scanned token doesn't match your current access token. Would you like to update your token?"
+                message += "\n\n" + String(localized: "The scanned token doesn't match your current access token. Would you like to update your token?")
             } else {
-                message = "The scanned token doesn't match your current access token. Would you like to update your token?"
+                message = String(localized: "The scanned token doesn't match your current access token. Would you like to update your token?")
             }
         }
 

+ 7 - 7
LoopFollow/Remote/Settings/RemoteSettingsView.swift

@@ -140,7 +140,7 @@ struct RemoteSettingsView: View {
                     HStack {
                         Text("Shared Secret")
                         TogglableSecureInput(
-                            placeholder: "Enter Shared Secret",
+                            placeholder: String(localized: "Enter Shared Secret"),
                             text: $viewModel.sharedSecret,
                             style: .singleLine
                         )
@@ -150,7 +150,7 @@ struct RemoteSettingsView: View {
                         HStack {
                             Text("APNS Key ID")
                             TogglableSecureInput(
-                                placeholder: "Enter APNS Key ID",
+                                placeholder: String(localized: "Enter APNS Key ID"),
                                 text: $viewModel.remoteKeyId,
                                 style: .singleLine
                             )
@@ -159,7 +159,7 @@ struct RemoteSettingsView: View {
                         VStack(alignment: .leading) {
                             Text("APNS Key")
                             TogglableSecureInput(
-                                placeholder: "Paste APNS Key",
+                                placeholder: String(localized: "Paste APNS Key"),
                                 text: $viewModel.remoteApnsKey,
                                 style: .multiLine
                             )
@@ -190,7 +190,7 @@ struct RemoteSettingsView: View {
                     HStack {
                         Text("Developer Team ID")
                         TogglableSecureInput(
-                            placeholder: "Enter Team ID",
+                            placeholder: String(localized: "Enter Team ID"),
                             text: $viewModel.loopDeveloperTeamId,
                             style: .singleLine
                         )
@@ -200,7 +200,7 @@ struct RemoteSettingsView: View {
                         HStack {
                             Text("APNS Key ID")
                             TogglableSecureInput(
-                                placeholder: "Enter APNS Key ID",
+                                placeholder: String(localized: "Enter APNS Key ID"),
                                 text: $viewModel.remoteKeyId,
                                 style: .singleLine
                             )
@@ -209,7 +209,7 @@ struct RemoteSettingsView: View {
                         VStack(alignment: .leading) {
                             Text("APNS Key")
                             TogglableSecureInput(
-                                placeholder: "Paste APNS Key",
+                                placeholder: String(localized: "Paste APNS Key"),
                                 text: $viewModel.remoteApnsKey,
                                 style: .multiLine
                             )
@@ -520,7 +520,7 @@ struct RemoteSettingsView: View {
             if let future = viewModel.diagnostics.futureStartDate {
                 diagnosticWarning(
                     title: String(localized: "Future-dated profile record found"),
-                    detail: "A profile record has startDate \(dateTimeUtils.formattedDate(from: future.startDate)). LoopFollow ignores future-dated records, but it will still appear as the current profile in your Nightscout dashboard. Consider deleting it — it usually means a phone with the wrong system clock is uploading."
+                    detail: String(localized: "A profile record has startDate \(dateTimeUtils.formattedDate(from: future.startDate)). LoopFollow ignores future-dated records, but it will still appear as the current profile in your Nightscout dashboard. Consider deleting it — it usually means a phone with the wrong system clock is uploading.")
                 )
             }
             if !viewModel.diagnostics.hasAnyWarning {

+ 7 - 7
LoopFollow/Remote/TRC/BolusView.swift

@@ -94,7 +94,7 @@ struct BolusView: View {
 
                     Section {
                         HKQuantityInputView(
-                            label: "Bolus Amount",
+                            label: String(localized: "Bolus Amount"),
                             quantity: $bolusAmount,
                             unit: .internationalUnit(),
                             maxLength: 5,
@@ -156,7 +156,7 @@ struct BolusView: View {
                         title: Text("Confirm Bolus"),
                         message: Text("Are you sure you want to send \(InsulinFormatter.shared.string(bolusAmount)) U?"),
                         primaryButton: .default(Text("Confirm"), action: {
-                            AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
+                            AuthService.authenticate(reason: String(localized: "Confirm your identity to send bolus.")) { result in
                                 DispatchQueue.main.async {
                                     switch result {
                                     case .success:
@@ -166,7 +166,7 @@ struct BolusView: View {
                                         self.alertType = .validation
                                         self.showAlert = true
                                     case .failed:
-                                        self.alertMessage = "Authentication failed"
+                                        self.alertMessage = String(localized: "Authentication failed")
                                         self.alertType = .validation
                                         self.showAlert = true
                                     case .canceled:
@@ -195,7 +195,7 @@ struct BolusView: View {
                 case .validation:
                     return Alert(
                         title: Text("Validation Error"),
-                        message: Text(alertMessage ?? "Invalid input."),
+                        message: Text(alertMessage ?? String(localized: "Invalid input.")),
                         dismissButton: .default(Text("OK"))
                     )
                 case .oldCalculationWarning:
@@ -281,7 +281,7 @@ struct BolusView: View {
         if isStale12 { return }
         if isStale5 {
             let mins = Int(ageSec / 60)
-            alertMessage = "This recommended bolus was calculated \(mins) minutes ago. New treatments may have occurred since then. Proceed with caution."
+            alertMessage = String(localized: "This recommended bolus was calculated \(mins) minutes ago. New treatments may have occurred since then. Proceed with caution.")
             alertType = .oldCalculationWarning
             showAlert = true
         } else {
@@ -322,7 +322,7 @@ struct BolusView: View {
                     if sentUnits > 0 {
                         QuickPickBolusesManager.shared.recordBolus(units: sentUnits)
                     }
-                    statusMessage = "Bolus command sent successfully."
+                    statusMessage = String(localized: "Bolus command sent successfully.")
                     LogManager.shared.log(
                         category: .apns,
                         message: "sendBolusPushNotification succeeded - Bolus: \(InsulinFormatter.shared.string(bolusAmount)) U"
@@ -330,7 +330,7 @@ struct BolusView: View {
                     bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0)
                     alertType = .statusSuccess
                 } else {
-                    statusMessage = errorMessage ?? "Failed to send bolus command."
+                    statusMessage = errorMessage ?? String(localized: "Failed to send bolus command.")
                     LogManager.shared.log(
                         category: .apns,
                         message: "sendBolusPushNotification failed with error: \(errorMessage ?? "unknown error")"

+ 11 - 11
LoopFollow/Remote/TRC/MealView.swift

@@ -115,7 +115,7 @@ struct MealView: View {
 
                         if mealWithFatProtein.value {
                             HKQuantityInputView(
-                                label: "Fat",
+                                label: String(localized: "Fat"),
                                 quantity: $fat,
                                 unit: .gram(),
                                 maxLength: 4,
@@ -128,7 +128,7 @@ struct MealView: View {
                             )
 
                             HKQuantityInputView(
-                                label: "Protein",
+                                label: String(localized: "Protein"),
                                 quantity: $protein,
                                 unit: .gram(),
                                 maxLength: 4,
@@ -143,7 +143,7 @@ struct MealView: View {
 
                         if mealWithBolus.value {
                             HKQuantityInputView(
-                                label: "Bolus Amount",
+                                label: String(localized: "Bolus Amount"),
                                 quantity: $bolusAmount,
                                 unit: .internationalUnit(),
                                 maxLength: 4,
@@ -161,7 +161,7 @@ struct MealView: View {
                         Toggle("Schedule for later", isOn: $isScheduling)
                         if isScheduling {
                             DatePicker(
-                                "Select Time",
+                                String(localized: "Select Time"),
                                 selection: Binding(
                                     get: { self.selectedTime ?? Date() },
                                     set: { self.selectedTime = $0 }
@@ -238,15 +238,15 @@ struct MealView: View {
                     let fatAmount = fat.doubleValue(for: HKUnit.gram())
                     let bolusAmount = bolusAmount.doubleValue(for: .internationalUnit())
 
-                    var message = "Are you sure you want to send the meal data"
+                    var message = String(localized: "Are you sure you want to send the meal data")
 
                     if let selectedTime = selectedTime {
                         let timeFormatter = DateFormatter()
                         timeFormatter.timeStyle = .short
                         let timeString = timeFormatter.string(from: selectedTime)
-                        message += " for \(timeString)?"
+                        message += String(localized: " for \(timeString)?")
                     } else {
-                        message += " now?"
+                        message += String(localized: " now?")
                     }
 
                     if carbsAmount > 0 {
@@ -271,7 +271,7 @@ struct MealView: View {
                         primaryButton: .default(Text("Confirm"), action: {
                             DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                 if bolusAmount > 0 {
-                                    AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
+                                    AuthService.authenticate(reason: String(localized: "Confirm your identity to send bolus.")) { result in
                                         DispatchQueue.main.async {
                                             switch result {
                                             case .success:
@@ -281,7 +281,7 @@ struct MealView: View {
                                                 self.alertType = .validationError
                                                 self.showAlert = true
                                             case .failed:
-                                                self.alertMessage = "Authentication failed"
+                                                self.alertMessage = String(localized: "Authentication failed")
                                                 self.alertType = .validationError
                                                 self.showAlert = true
                                             case .canceled:
@@ -362,7 +362,7 @@ struct MealView: View {
                             bolus: bolusAmount.doubleValue(for: .internationalUnit())
                         )
                     }
-                    statusMessage = "Meal command sent successfully."
+                    statusMessage = String(localized: "Meal command sent successfully.")
                     LogManager.shared.log(
                         category: .apns,
                         message: "sendMealPushNotification succeeded - Carbs: \(carbs.doubleValue(for: .gram())) g, Protein: \(protein.doubleValue(for: .gram())) g, Fat: \(fat.doubleValue(for: .gram())) g, Bolus: \(bolusAmount.doubleValue(for: .internationalUnit())) U, Scheduled: \(scheduledDate != nil ? formatDate(scheduledDate!) : "now")"
@@ -376,7 +376,7 @@ struct MealView: View {
                     isScheduling = false
                     alertType = .statusSuccess
                 } else {
-                    statusMessage = errorMessage ?? "Failed to send meal command."
+                    statusMessage = errorMessage ?? String(localized: "Failed to send meal command.")
                     LogManager.shared.log(
                         category: .apns,
                         message: "sendMealPushNotification failed with error: \(errorMessage ?? "unknown error")"

+ 7 - 7
LoopFollow/Remote/TRC/OverrideView.swift

@@ -37,7 +37,7 @@ struct OverrideView: View {
             VStack {
                 if device.value != "Trio" {
                     ErrorMessageView(
-                        message: "Remote commands are currently only available for Trio."
+                        message: String(localized: "Remote commands are currently only available for Trio.")
                     )
                 } else {
                     Form {
@@ -147,13 +147,13 @@ struct OverrideView: View {
                 case .statusFailure:
                     return Alert(
                         title: Text("Error"),
-                        message: Text(statusMessage ?? "An error occurred."),
+                        message: Text(statusMessage ?? String(localized: "An error occurred.")),
                         dismissButton: .default(Text("OK"))
                     )
                 case .validation:
                     return Alert(
                         title: Text("Validation Error"),
-                        message: Text(alertMessage ?? "Invalid input."),
+                        message: Text(alertMessage ?? String(localized: "Invalid input.")),
                         dismissButton: .default(Text("OK"))
                     )
                 case .none:
@@ -172,11 +172,11 @@ struct OverrideView: View {
             DispatchQueue.main.async {
                 self.isLoading = false
                 if success {
-                    self.statusMessage = "Override command sent successfully."
+                    self.statusMessage = String(localized: "Override command sent successfully.")
                     self.alertType = .statusSuccess
                     LogManager.shared.log(category: .apns, message: "sendOverridePushNotification succeeded for override: \(override.name)")
                 } else {
-                    self.statusMessage = errorMessage ?? "Failed to send override command."
+                    self.statusMessage = errorMessage ?? String(localized: "Failed to send override command.")
                     self.alertType = .statusFailure
                     LogManager.shared.log(category: .apns, message: "sendOverridePushNotification failed for override: \(override.name). Error: \(errorMessage ?? "unknown error")")
                 }
@@ -192,11 +192,11 @@ struct OverrideView: View {
             DispatchQueue.main.async {
                 self.isLoading = false
                 if success {
-                    self.statusMessage = "Cancel override command sent successfully."
+                    self.statusMessage = String(localized: "Cancel override command sent successfully.")
                     self.alertType = .statusSuccess
                     LogManager.shared.log(category: .apns, message: "sendCancelOverridePushNotification succeeded")
                 } else {
-                    self.statusMessage = errorMessage ?? "Failed to send cancel override command."
+                    self.statusMessage = errorMessage ?? String(localized: "Failed to send cancel override command.")
                     self.alertType = .statusFailure
                     LogManager.shared.log(category: .apns, message: "sendCancelOverridePushNotification failed. Error: \(errorMessage ?? "unknown error")")
                 }

+ 6 - 6
LoopFollow/Remote/TRC/TempTargetView.swift

@@ -39,7 +39,7 @@ struct TempTargetView: View {
             VStack {
                 if device.value != "Trio" {
                     ErrorMessageView(
-                        message: "Remote commands are currently only available for Trio."
+                        message: String(localized: "Remote commands are currently only available for Trio.")
                     )
                 } else {
                     Form {
@@ -204,7 +204,7 @@ struct TempTargetView: View {
                 case .validation:
                     return Alert(
                         title: Text("Validation Error"),
-                        message: Text(alertMessage ?? "Invalid input."),
+                        message: Text(alertMessage ?? String(localized: "Invalid input.")),
                         dismissButton: .default(Text("OK"))
                     )
                 case .none:
@@ -252,11 +252,11 @@ struct TempTargetView: View {
             DispatchQueue.main.async {
                 self.isLoading = false
                 if success {
-                    self.statusMessage = "Temp target command successfully sent."
+                    self.statusMessage = String(localized: "Temp target command successfully sent.")
                     self.alertType = .statusSuccess
                     LogManager.shared.log(category: .apns, message: "sendTempTargetPushNotification succeeded with target: \(newHKTarget), duration: \(duration)")
                 } else {
-                    self.statusMessage = errorMessage ?? "Failed to send temp target command."
+                    self.statusMessage = errorMessage ?? String(localized: "Failed to send temp target command.")
                     self.alertType = .statusFailure
                     LogManager.shared.log(category: .apns, message: "sendTempTargetPushNotification failed with target: \(newHKTarget), duration: \(duration), error: \(errorMessage ?? "unknown error")")
                 }
@@ -272,11 +272,11 @@ struct TempTargetView: View {
             DispatchQueue.main.async {
                 self.isLoading = false
                 if success {
-                    self.statusMessage = "Cancel temp target command successfully sent."
+                    self.statusMessage = String(localized: "Cancel temp target command successfully sent.")
                     self.alertType = .statusSuccess
                     LogManager.shared.log(category: .apns, message: "sendCancelTempTargetPushNotification succeeded")
                 } else {
-                    self.statusMessage = errorMessage ?? "Failed to send cancel temp target command."
+                    self.statusMessage = errorMessage ?? String(localized: "Failed to send cancel temp target command.")
                     self.alertType = .statusFailure
                     LogManager.shared.log(category: .apns, message: "sendCancelTempTargetPushNotification failed with error: \(errorMessage ?? "unknown error")")
                 }

+ 1 - 1
LoopFollow/Remote/TRC/TrioRemoteControlView.swift

@@ -31,7 +31,7 @@ struct TrioRemoteControlView: View {
 }
 
 struct CommandButtonView<Destination: View>: View {
-    let command: String
+    let command: LocalizedStringKey
     let iconName: String
     let destination: Destination
 

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 6300 - 4727
LoopFollow/Resources/Localizable.xcstrings


+ 2 - 2
LoopFollow/Snoozer/SnoozerView.swift

@@ -594,7 +594,7 @@ struct SnoozerView: View {
         NavigationView {
             VStack {
                 DatePicker(
-                    "Snooze until (date)",
+                    String(localized: "Snooze until (date)"),
                     selection: snoozeUntilBindingForDate(),
                     displayedComponents: [.date]
                 )
@@ -617,7 +617,7 @@ struct SnoozerView: View {
         NavigationView {
             VStack {
                 DatePicker(
-                    "Snooze until (time)",
+                    String(localized: "Snooze until (time)"),
                     selection: snoozeUntilBindingForTime(),
                     displayedComponents: [.hourAndMinute]
                 )

+ 1 - 1
LoopFollow/Stats/AGP/AGPGraphView.swift

@@ -86,7 +86,7 @@ struct AGPGraphView: UIViewRepresentable {
         p25DataSet.drawFilledEnabled = false
         p25DataSet.mode = .linear
 
-        let p50DataSet = LineChartDataSet(entries: sortedP50, label: "Median")
+        let p50DataSet = LineChartDataSet(entries: sortedP50, label: String(localized: "Median"))
         p50DataSet.colors = [NSUIColor.systemBlue]
         p50DataSet.lineWidth = 3
         p50DataSet.drawCirclesEnabled = false

+ 1 - 1
LoopFollow/Stats/AGP/AGPView.swift

@@ -22,7 +22,7 @@ struct AGPView: View {
                 HStack(spacing: 16) {
                     LegendItem(color: .gray.opacity(0.6), label: "5th-95th")
                     LegendItem(color: .blue.opacity(0.7), label: "25th-75th")
-                    LegendItem(color: .blue, label: "Median")
+                    LegendItem(color: .blue, label: String(localized: "Median"))
                 }
                 .font(.caption2)
             }

+ 6 - 6
LoopFollow/Stats/AggregatedStatsView.swift

@@ -275,9 +275,9 @@ struct InsulinTotalsCard: View {
 
             VStack(spacing: 10) {
                 metricRow(title: String(localized: "Programmed Basal"), value: programmedBasal, color: .indigo)
-                metricRow(title: "Total Negative Basal", value: totalNegativeBasal, color: .red)
-                metricRow(title: "Total Positive Basal", value: totalPositiveBasal, color: .green)
-                metricRow(title: "Actual Basal", value: actualBasal, color: .indigo)
+                metricRow(title: String(localized: "Total Negative Basal"), value: totalNegativeBasal, color: .red)
+                metricRow(title: String(localized: "Total Positive Basal"), value: totalPositiveBasal, color: .green)
+                metricRow(title: String(localized: "Actual Basal"), value: actualBasal, color: .indigo)
                 metricRow(title: String(localized: "Avg Bolus"), value: avgBolus, color: .purple)
                 metricRow(title: String(localized: "Total Daily Dose"), value: totalDailyDose, color: .pink)
             }
@@ -367,7 +367,7 @@ struct StatsGridView: View {
                     StatCard(
                         title: String(localized: "Avg Carbs"),
                         value: formatCarbs(simpleStats.avgCarbs),
-                        unit: "g/day",
+                        unit: String(localized: "g/day"),
                         color: .yellow
                     )
                 }
@@ -376,7 +376,7 @@ struct StatsGridView: View {
                     StatCard(
                         title: String(localized: "Programmed Basal"),
                         value: formatInsulin(simpleStats.programmedBasal),
-                        unit: "U/day",
+                        unit: String(localized: "U/day"),
                         color: .indigo
                     )
                     .onTapGesture(count: 3) {
@@ -386,7 +386,7 @@ struct StatsGridView: View {
                     StatCard(
                         title: String(localized: "Avg Bolus"),
                         value: formatInsulin(simpleStats.avgBolus),
-                        unit: "U/day",
+                        unit: String(localized: "U/day"),
                         color: .purple
                     )
                     .onTapGesture(count: 3) {

+ 4 - 4
LoopFollow/Stats/DataAvailabilityCalculator.swift

@@ -27,10 +27,10 @@ struct DataAvailabilityInfo {
 
         var description: String {
             switch self {
-            case .excellent: return "Excellent"
-            case .good: return "Good"
-            case .fair: return "Fair"
-            case .poor: return "Poor"
+            case .excellent: return String(localized: "Excellent")
+            case .good: return String(localized: "Good")
+            case .fair: return String(localized: "Fair")
+            case .poor: return String(localized: "Poor")
             }
         }
     }

+ 2 - 2
LoopFollow/Stats/DateRangePicker.swift

@@ -144,7 +144,7 @@ struct DateRangePicker: View {
 
                     if showStartDatePicker {
                         DatePicker(
-                            "Select Start Date",
+                            String(localized: "Select Start Date"),
                             selection: $startDate,
                             in: ...endDate,
                             displayedComponents: [.date]
@@ -159,7 +159,7 @@ struct DateRangePicker: View {
 
                     if showEndDatePicker {
                         DatePicker(
-                            "Select End Date",
+                            String(localized: "Select End Date"),
                             selection: $endDate,
                             in: startDate ... lastFullDay,
                             displayedComponents: [.date]

+ 6 - 6
LoopFollow/Stats/GRI/GRIRiskGridView.swift

@@ -84,33 +84,33 @@ struct GRIRiskGridView: UIViewRepresentable {
             }
         }
 
-        let zoneADataSet = ScatterChartDataSet(entries: zoneAEntries, label: "Zone A")
+        let zoneADataSet = ScatterChartDataSet(entries: zoneAEntries, label: String(localized: "Zone A"))
         zoneADataSet.setColor(NSUIColor.systemGreen.withAlphaComponent(0.3))
         zoneADataSet.scatterShapeSize = 4
         zoneADataSet.drawValuesEnabled = false
 
-        let zoneBDataSet = ScatterChartDataSet(entries: zoneBEntries, label: "Zone B")
+        let zoneBDataSet = ScatterChartDataSet(entries: zoneBEntries, label: String(localized: "Zone B"))
         zoneBDataSet.setColor(NSUIColor.systemYellow.withAlphaComponent(0.3))
         zoneBDataSet.scatterShapeSize = 4
         zoneBDataSet.drawValuesEnabled = false
 
-        let zoneCDataSet = ScatterChartDataSet(entries: zoneCEntries, label: "Zone C")
+        let zoneCDataSet = ScatterChartDataSet(entries: zoneCEntries, label: String(localized: "Zone C"))
         zoneCDataSet.setColor(NSUIColor.systemOrange.withAlphaComponent(0.3))
         zoneCDataSet.scatterShapeSize = 4
         zoneCDataSet.drawValuesEnabled = false
 
-        let zoneDDataSet = ScatterChartDataSet(entries: zoneDEntries, label: "Zone D")
+        let zoneDDataSet = ScatterChartDataSet(entries: zoneDEntries, label: String(localized: "Zone D"))
         zoneDDataSet.setColor(NSUIColor.systemRed.withAlphaComponent(0.3))
         zoneDDataSet.scatterShapeSize = 4
         zoneDDataSet.drawValuesEnabled = false
 
-        let zoneEDataSet = ScatterChartDataSet(entries: zoneEEntries, label: "Zone E")
+        let zoneEDataSet = ScatterChartDataSet(entries: zoneEEntries, label: String(localized: "Zone E"))
         zoneEDataSet.setColor(NSUIColor.systemRed.withAlphaComponent(0.5))
         zoneEDataSet.scatterShapeSize = 4
         zoneEDataSet.drawValuesEnabled = false
 
         let currentPoint = ChartDataEntry(x: xHypoValue, y: yHyperValue)
-        let currentDataSet = ScatterChartDataSet(entries: [currentPoint], label: "Current GRI")
+        let currentDataSet = ScatterChartDataSet(entries: [currentPoint], label: String(localized: "Current GRI"))
         currentDataSet.setColor(NSUIColor.label)
         currentDataSet.scatterShapeSize = 12
         currentDataSet.setScatterShape(.circle)

+ 5 - 5
LoopFollow/Stats/GRI/GRIView.swift

@@ -103,15 +103,15 @@ struct GRIView: View {
 
     private func griZone(_ gri: Double) -> String {
         if gri <= 20 {
-            return "Zone A"
+            return String(localized: "Zone A")
         } else if gri <= 40 {
-            return "Zone B"
+            return String(localized: "Zone B")
         } else if gri <= 60 {
-            return "Zone C"
+            return String(localized: "Zone C")
         } else if gri <= 80 {
-            return "Zone D"
+            return String(localized: "Zone D")
         } else {
-            return "Zone E"
+            return String(localized: "Zone E")
         }
     }
 }

+ 3 - 3
LoopFollow/Stats/TIR/TIRGraphView.swift

@@ -67,10 +67,10 @@ struct TIRGraphView: UIViewRepresentable {
                 ]
             )
             dataEntries.append(entry)
-            xAxisLabels.append(point.period.rawValue)
+            xAxisLabels.append(String(localized: String.LocalizationValue(point.period.rawValue)))
         }
 
-        let dataSet = BarChartDataSet(entries: dataEntries, label: "Time in Range")
+        let dataSet = BarChartDataSet(entries: dataEntries, label: String(localized: "Time in Range"))
         dataSet.colors = [
             UIColor.systemRed.withAlphaComponent(0.8),
             UIColor.systemRed.withAlphaComponent(0.5),
@@ -78,7 +78,7 @@ struct TIRGraphView: UIViewRepresentable {
             UIColor.systemYellow.withAlphaComponent(0.7),
             UIColor.systemOrange.withAlphaComponent(0.7),
         ]
-        dataSet.stackLabels = ["Very Low", "Low", "In Range", "High", "Very High"]
+        dataSet.stackLabels = [String(localized: "Very Low"), String(localized: "Low"), String(localized: "In Range"), String(localized: "High"), String(localized: "Very High")]
         dataSet.drawValuesEnabled = false
 
         let data = BarChartData(dataSet: dataSet)

+ 8 - 8
LoopFollow/Stats/TIR/TIRView.swift

@@ -37,27 +37,27 @@ struct TIRView: View {
 
                                 TIRLegendItem(
                                     color: .orange,
-                                    label: "Very High (\(veryHighCutoffText))",
+                                    label: String(localized: "Very High (\(veryHighCutoffText))"),
                                     percentage: average.veryHigh
                                 )
                                 TIRLegendItem(
                                     color: .yellow,
-                                    label: "High (\(highCutoffText))",
+                                    label: String(localized: "High (\(highCutoffText))"),
                                     percentage: average.high
                                 )
                                 TIRLegendItem(
                                     color: .green,
-                                    label: "In Range (\(inRangeCutoffText))",
+                                    label: String(localized: "In Range (\(inRangeCutoffText))"),
                                     percentage: average.inRange
                                 )
                                 TIRLegendItem(
                                     color: .red.opacity(0.5),
-                                    label: "Low (\(lowCutoffText))",
+                                    label: String(localized: "Low (\(lowCutoffText))"),
                                     percentage: average.low
                                 )
                                 TIRLegendItem(
                                     color: .red.opacity(0.8),
-                                    label: "Very Low (\(veryLowCutoffText))",
+                                    label: String(localized: "Very Low (\(veryLowCutoffText))"),
                                     percentage: average.veryLow
                                 )
                             }
@@ -87,11 +87,11 @@ struct TIRView: View {
     private var tirTitle: String {
         switch UnitSettingsStore.shared.timeInRangeMode {
         case .tir:
-            return "Time in Range"
+            return String(localized: "Time in Range")
         case .titr:
-            return "Time in Tight Range"
+            return String(localized: "Time in Tight Range")
         case .custom:
-            return "Custom Range"
+            return String(localized: "Custom Range")
         }
     }
 

+ 184 - 0
Scripts/gen_zh_catalog.py

@@ -803,6 +803,190 @@ add([
      "这些按钮显示你最近最常用的餐食。\n\n它们基于你以往在相近日子、相近时间发送过的内容——比如你通常在工作日早晨发送同样的早餐,它就会作为选项出现。\n\n点按按钮即可填入详情。在你查看并确认之前,不会发送任何内容。"),
 ])
 
+# ==== 二轮深查:主页面 ====
+add([
+    ("⚠️ Not Looping!", "⚠️ 未闭环!"),
+    ("LOW", "低"),
+    ("HIGH", "高"),
+    ("Est A1C:", "估算 A1C:"),
+    ("Est. A1C:", "估算 A1C:"),
+    ("Std Dev:", "标准差:"),
+    ("Low:", "低:"),
+    ("In Range:", "范围内:"),
+    ("High:", "高:"),
+    ("Avg BG:", "平均血糖:"),
+    ("Updated", "更新时间"),
+    ("LoopFollow Background Refresh", "LoopFollow 后台刷新"),
+    ("App inactive for 6 minutes. Verify Bluetooth connectivity.", "应用已 6 分钟未活动。请检查蓝牙连接。"),
+    ("App inactive for 6 minutes. Open to resume.", "应用已 6 分钟未活动。请打开应用以恢复。"),
+    ("App inactive for 12 minutes. Verify Bluetooth connectivity.", "应用已 12 分钟未活动。请检查蓝牙连接。"),
+    ("App inactive for 12 minutes. Open to resume.", "应用已 12 分钟未活动。请打开应用以恢复。"),
+    ("App inactive for 18 minutes. Verify Bluetooth connectivity.", "应用已 18 分钟未活动。请检查蓝牙连接。"),
+    ("App inactive for 18 minutes. Open to resume.", "应用已 18 分钟未活动。请打开应用以恢复。"),
+    ("Suspend Pump", "暂停泵"),
+    ("Resume Pump", "恢复泵"),
+    ("Start Sensor", "启动传感器"),
+    ("Entered By: ", "录入者:"),
+    ("Bolus:", "大剂量:"),
+    ("Carbs:", "碳水:"),
+    ("Absorption:", "吸收时间:"),
+    ("hrs", "小时"),
+    ("From:", "来自:"),
+])
+
+# ==== 二轮深查:远程控制(Trio TRC + Loop APNS)====
+add([
+    ("Bolus Amount", "大剂量数值"),
+    ("Insulin Amount", "胰岛素数值"),
+    ("Carbs Amount", "碳水数值"),
+    ("Fat", "脂肪"),
+    ("Protein", "蛋白质"),
+    ("Select Time", "选择时间"),
+    ("Confirm your identity to send bolus.", "确认你的身份以发送大剂量。"),
+    ("Confirm your identity to send insulin.", "确认你的身份以发送胰岛素。"),
+    ("Authentication failed", "身份验证失败"),
+    ("Bolus command sent successfully.", "大剂量指令已发送成功。"),
+    ("Failed to send bolus command.", "大剂量指令发送失败。"),
+    ("Meal command sent successfully.", "餐食指令已发送成功。"),
+    ("Failed to send meal command.", "餐食指令发送失败。"),
+    ("Temp target command successfully sent.", "临时目标指令已发送成功。"),
+    ("Failed to send temp target command.", "临时目标指令发送失败。"),
+    ("Cancel temp target command successfully sent.", "取消临时目标指令已发送成功。"),
+    ("Failed to send cancel temp target command.", "取消临时目标指令发送失败。"),
+    ("Override command sent successfully.", "临时配置指令已发送成功。"),
+    ("Failed to send override command.", "临时配置指令发送失败。"),
+    ("Cancel override command sent successfully.", "取消临时配置指令已发送成功。"),
+    ("Failed to send cancel override command.", "取消临时配置指令发送失败。"),
+    ("Active override cancelled successfully.", "当前临时配置已成功取消。"),
+    ("An error occurred.", "发生错误。"),
+    ("Invalid input.", "输入无效。"),
+    ("Please wait...", "请稍候…"),
+    ("Remote commands are currently only available for Trio.", "远程指令目前仅支持 Trio。"),
+    ("Are you sure you want to send the meal data", "确定要发送该餐食数据"),
+    (" for %@?", ",用餐时间 %@ 吗?"),
+    (" now?", "吗?"),
+    ("This recommended bolus was calculated %lld minutes ago. New treatments may have occurred since then. Proceed with caution.",
+     "该推荐大剂量计算于 %lld 分钟前。此后可能已有新的治疗发生,请谨慎操作。"),
+    ("WARNING: New treatments may have occurred since the last recommended bolus was calculated %@ ago.",
+     "警告:推荐大剂量计算于 %@ 前,此后可能已有新的治疗发生。"),
+    ("Calculated %lld minute%@ ago", "计算于 %1$lld 分钟前"),
+    ("Indefinite", "无限期"),
+    ("Nightscout URL and token not configured in settings", "设置中未配置 Nightscout 网址和令牌"),
+    ("Invalid response from server", "服务器响应无效"),
+    ("Loop APNS Configuration not valid", "Loop APNS 配置无效"),
+    ("Failed generating JWT token, check APNS Key ID, APNS Key and Team ID", "生成 JWT 令牌失败,请检查 APNS 密钥 ID、APNS 密钥和团队 ID"),
+    ("Network error occurred", "发生网络错误"),
+    ("No device token found in profile", "配置文件中未找到设备令牌"),
+    ("Unauthorized - check your API secret", "未授权——请检查你的 API 密钥"),
+    ("Device token not configured", "设备令牌未配置"),
+    ("Too many requests - please wait a few minutes before trying again", "请求过于频繁——请等待几分钟后再试"),
+    ("Loop APNS setup is incomplete. Please configure all required fields in settings.", "Loop APNS 设置不完整。请在设置中填写所有必填项。"),
+    ("Invalid QR code URL. Please re-scan the QR code in settings.", "二维码网址无效。请在设置中重新扫描二维码。"),
+    ("Please enter a valid insulin amount", "请输入有效的胰岛素数值"),
+    ("Please enter a valid carb amount", "请输入有效的碳水数值"),
+    ("Insulin sent successfully!", "胰岛素已发送成功!"),
+    ("Failed to send insulin. Check your Loop APNS configuration.", "胰岛素发送失败。请检查你的 Loop APNS 配置。"),
+    ("Failed to send carbs. Check your Loop APNS configuration.", "碳水发送失败。请检查你的 Loop APNS 配置。"),
+    ("Carbs sent successfully for %@!", "碳水已发送成功(用餐时间 %@)!"),
+    ("Carbs amount (%lldg) exceeds the maximum allowed (%lldg). Please reduce the amount.",
+     "碳水数量(%1$lld 克)超过允许的最大值(%2$lld 克)。请减少数量。"),
+    ("Time must be within the prior %lld hours", "时间必须在过去 %lld 小时之内"),
+    ("Time must be within the next %lld hour", "时间必须在未来 %lld 小时之内"),
+    ("The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?",
+     "扫描到的设置包含一个 Nightscout 网址。要将它设为你的 Nightscout 地址吗?"),
+    ("The scanned settings also include a token. Would you like to set this as your access token?",
+     "扫描到的设置还包含一个令牌。要将它设为你的访问令牌吗?"),
+    ("The scanned settings include a token. Would you like to set this as your access token?",
+     "扫描到的设置包含一个令牌。要将它设为你的访问令牌吗?"),
+    ("The scanned token doesn't match your current access token. Would you like to update your token?",
+     "扫描到的令牌与当前访问令牌不一致。要更新你的令牌吗?"),
+    ("Enter Shared Secret", "输入共享密钥"),
+    ("Enter APNS Key ID", "输入 APNS 密钥 ID"),
+    ("Paste APNS Key", "粘贴 APNS 密钥"),
+    ("Enter Team ID", "输入团队 ID"),
+    ("Enter Token", "输入令牌"),
+    ("A profile record has startDate %@. LoopFollow ignores future-dated records, but it will still appear as the current profile in your Nightscout dashboard. Consider deleting it — it usually means a phone with the wrong system clock is uploading.",
+     "有一条配置文件记录的开始日期为 %@。LoopFollow 会忽略未来日期的记录,但它仍会显示为你 Nightscout 面板中的当前配置文件。建议删除——这通常意味着有一部系统时钟错误的手机正在上传。"),
+    ("Diagnostics unavailable: %@", "诊断不可用:%@"),
+    ("Device tokens are alternating in recent profile uploads (%lld tokens involved across %lld records). This usually means more than one app installation is uploading to the same Nightscout. Remove the app from spare or unused phones.",
+     "近期的配置文件上传中设备令牌交替出现(%2$lld 条记录中涉及 %1$lld 个令牌)。这通常意味着有多个应用安装在向同一个 Nightscout 上传。请从闲置手机上移除该应用。"),
+    ("Do you want to activate the override '%@'?", "要启用临时配置「%@」吗?"),
+    ("Percentage: %lld%", "比例:%lld%"),
+    ("Percentage: %lld%%", "比例:%lld%%"),
+    ("Target: %@ %@", "目标:%1$@ %2$@"),
+    ("Target: %@-%@", "目标:%1$@-%2$@"),
+    ("Insulin: %lld%", "胰岛素:%lld%"),
+    ("Insulin: %lld%%", "胰岛素:%lld%%"),
+    ("New Target: %@ %@\nDuration: %lld minutes", "新目标:%1$@ %2$@\n持续时间:%3$lld 分钟"),
+])
+
+# ==== 二轮深查:Nightscout 状态 ====
+add([
+    ("Checking...", "正在检查…"),
+    ("Disconnected", "未连接"),
+    ("Connecting...", "正在连接…"),
+    ("Invalid URL", "网址无效"),
+    ("Network Error", "网络错误"),
+    ("Invalid Token", "令牌无效"),
+    ("Token Required", "需要令牌"),
+    ("Site Not Found", "未找到站点"),
+    ("Unknown Error", "未知错误"),
+    ("Address Empty", "地址为空"),
+    ("Admin", "管理员"),
+    ("OK (%@)", "正常(%@)"),
+])
+
+# ==== 二轮深查:统计页 ====
+add([
+    ("Loading data...", "正在加载数据…"),
+    ("Total Negative Basal", "负向基础率合计"),
+    ("Total Positive Basal", "正向基础率合计"),
+    ("Actual Basal", "实际基础率"),
+    ("g/day", "克/天"),
+    ("U/day", "U/天"),
+    ("Very High (%@)", "非常高(%@)"),
+    ("High (%@)", "高(%@)"),
+    ("In Range (%@)", "范围内(%@)"),
+    ("Low (%@)", "低(%@)"),
+    ("Very Low (%@)", "非常低(%@)"),
+    ("Time in Range", "目标范围内时间"),
+    ("Time in Tight Range", "严格范围内时间"),
+    ("Custom Range", "自定义范围"),
+    ("Very Low", "非常低"),
+    ("In Range", "范围内"),
+    ("Very High", "非常高"),
+    ("Morning", "上午"),
+    ("Evening", "傍晚"),
+    ("Average", "平均"),
+    ("Zone A", "区域 A"),
+    ("Zone B", "区域 B"),
+    ("Zone C", "区域 C"),
+    ("Zone D", "区域 D"),
+    ("Zone E", "区域 E"),
+    ("Current GRI", "当前 GRI"),
+    ("Median", "中位数"),
+    ("Excellent", "极好"),
+    ("Good", "良好"),
+    ("Fair", "一般"),
+    ("Poor", "较差"),
+    ("Select Start Date", "选择开始日期"),
+    ("Select End Date", "选择结束日期"),
+    ("Cutoffs in %@", "阈值单位:%@"),
+])
+
+# ==== 二轮深查:实时活动设置与其它 ====
+add([
+    ("Top left", "左上"),
+    ("Top right", "右上"),
+    ("Bottom left", "左下"),
+    ("Bottom right", "右下"),
+    ("Live Activity Restarted", "实时活动已重启"),
+    ("Restart Live Activity", "重启实时活动"),
+    ("Snooze until (date)", "暂停至(日期)"),
+    ("Snooze until (time)", "暂停至(时间)"),
+    ("Unknown", "未知"),
+])
+
 # ---------- 写出 ----------
 strings = collections.OrderedDict()
 for en in sorted(T.keys()):