Ver código fonte

合并 7.0.0 与最新中文本地化

q5078620 2 dias atrás
pai
commit
5fef44dc7d
46 arquivos alterados com 7163 adições e 6030 exclusões
  1. 1 1
      LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift
  2. 1 1
      LoopFollow/Alarm/Alarm.swift
  3. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift
  4. 1 2
      LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift
  5. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift
  6. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift
  7. 1 3
      LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift
  8. 1 3
      LoopFollow/Alarm/AlarmEditing/Editors/FutureCarbsAlarmEditor.swift
  9. 1 2
      LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift
  10. 1 2
      LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift
  11. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift
  12. 1 3
      LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift
  13. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift
  14. 1 2
      LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift
  15. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift
  16. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift
  17. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift
  18. 1 3
      LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift
  19. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift
  20. 1 2
      LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift
  21. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift
  22. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift
  23. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift
  24. 1 1
      LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift
  25. 4 4
      LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift
  26. 2 2
      LoopFollow/BackgroundRefresh/BT/BLEManager.swift
  27. 2 2
      LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift
  28. 2 2
      LoopFollow/Contact/ContactColorMode.swift
  29. 5 5
      LoopFollow/Helpers/AuthService.swift
  30. 2 2
      LoopFollow/Helpers/BuildDetails.swift
  31. 7 7
      LoopFollow/Helpers/NightscoutUtils.swift
  32. 2 2
      LoopFollow/Helpers/PredictionDisplayType.swift
  33. 3 3
      LoopFollow/Helpers/TextFieldWithToolBar.swift
  34. 1 1
      LoopFollow/Log/SearchBar.swift
  35. 6900 5909
      LoopFollow/Resources/Localizable.xcstrings
  36. 8 8
      LoopFollow/Settings/ImportExport/ExportableSettings.swift
  37. 3 3
      LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift
  38. 12 12
      LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift
  39. 1 1
      LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift
  40. 1 1
      LoopFollow/Snoozer/SnoozerViewModel.swift
  41. 1 1
      LoopFollow/Storage/Observable.swift
  42. 1 1
      LoopFollow/Task/MinAgoTask.swift
  43. 3 3
      LoopFollow/Treatments/TreatmentsView.swift
  44. 5 5
      LoopFollow/ViewControllers/MainViewController.swift
  45. 2 2
      LoopFollow/ViewControllers/MoreMenuView.swift
  46. 172 17
      Scripts/gen_zh_catalog.py

+ 1 - 1
LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift

@@ -28,7 +28,7 @@ struct AddAlarmSheet: View {
                     ForEach(AlarmType.Group.allCases, id: \.self) { group in
                     ForEach(AlarmType.Group.allCases, id: \.self) { group in
                         let types = AlarmType.allCases.filter { $0.group == group && matches($0) }
                         let types = AlarmType.allCases.filter { $0.group == group && matches($0) }
                         if !types.isEmpty {
                         if !types.isEmpty {
-                            Section(header: Text(group.rawValue)
+                            Section(header: Text(LocalizedStringKey(group.rawValue))
                                 .font(.headline)
                                 .font(.headline)
                                 .frame(maxWidth: .infinity, alignment: .leading)
                                 .frame(maxWidth: .infinity, alignment: .leading)
                                 .padding(.horizontal, 4)
                                 .padding(.horizontal, 4)

+ 1 - 1
LoopFollow/Alarm/Alarm.swift

@@ -289,7 +289,7 @@ struct Alarm: Identifiable, Codable, Equatable {
 
 
     init(type: AlarmType) {
     init(type: AlarmType) {
         self.type = type
         self.type = type
-        name = type.rawValue
+        name = type.localizedTitle
 
 
         switch type {
         switch type {
         case .buildExpire:
         case .buildExpire:

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift

@@ -9,7 +9,7 @@ struct BatteryDropAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "This warns you if your phone’s battery drops quickly, based on the percentage and time you set.",
+                text: String(localized: "This warns you if your phone’s battery drops quickly, based on the percentage and time you set."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 2
LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift

@@ -9,8 +9,7 @@ struct BuildExpireAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Sends a reminder before the looping-app build you’re following reaches its "
-                    + "TestFlight or Xcode expiry date. Works with Trio 0.4 and later."
+                text: String(localized: "Sends a reminder before the looping-app build you’re following reaches its TestFlight or Xcode expiry date. Works with Trio 0.4 and later.")
             )
             )
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift

@@ -9,7 +9,7 @@ struct COBAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when Carbs-on-Board is at or above the amount you set below.",
+                text: String(localized: "Alerts when Carbs-on-Board is at or above the amount you set below."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift

@@ -11,7 +11,7 @@ struct FastDropAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when glucose readings drop rapidly. For example, three straight readings each falling by at least the amount you set. Optionally limit alerts to only fire below a certain BG level."
+                text: String(localized: "Alerts when glucose readings drop rapidly. For example, three straight readings each falling by at least the amount you set. Optionally limit alerts to only fire below a certain BG level.")
             )
             )
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 3
LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift

@@ -9,9 +9,7 @@ struct FastRiseAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when glucose readings rise rapidly. For example, "
-                    + "three straight readings each climbing by at least the amount "
-                    + "you set.  Optionally limit alerts to only fire above a certain BG.",
+                text: String(localized: "Alerts when glucose readings rise rapidly. For example, three straight readings each climbing by at least the amount you set.  Optionally limit alerts to only fire above a certain BG."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 3
LoopFollow/Alarm/AlarmEditing/Editors/FutureCarbsAlarmEditor.swift

@@ -9,9 +9,7 @@ struct FutureCarbsAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when a future-dated carb entry's scheduled time arrives — " +
-                    "a reminder to start eating. Use the max lookahead to ignore " +
-                    "fat/protein entries that are typically scheduled further ahead.",
+                text: String(localized: "Alerts when a future-dated carb entry's scheduled time arrives — a reminder to start eating. Use the max lookahead to ignore fat/protein entries that are typically scheduled further ahead."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 2
LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift

@@ -9,8 +9,7 @@ struct HighBgAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when glucose stays above the limit "
-                    + "you set below. Use Persistent if you want to ignore brief spikes."
+                text: String(localized: "Alerts when glucose stays above the limit you set below. Use Persistent if you want to ignore brief spikes.")
             )
             )
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)

+ 1 - 2
LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift

@@ -9,8 +9,7 @@ struct IOBAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when insulin-on-board is high, or when several "
-                    + "boluses in quick succession exceed the limits you set.",
+                text: String(localized: "Alerts when insulin-on-board is high, or when several boluses in quick succession exceed the limits you set."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift

@@ -8,7 +8,7 @@ struct LowBgAlarmEditor: View {
 
 
     var body: some View {
     var body: some View {
         Group {
         Group {
-            InfoBanner(text: "This warns you if the glucose is too low now or might be soon, based on the forecast.")
+            InfoBanner(text: String(localized: "This warns you if the glucose is too low now or might be soon, based on the forecast."))
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 3
LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift

@@ -9,9 +9,7 @@ struct MissedBolusAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when carbs are logged but no bolus is delivered " +
-                    "within the delay below.  Allows small-carb / treatment " +
-                    "exclusions and pre-bolus detection.",
+                text: String(localized: "Alerts when carbs are logged but no bolus is delivered within the delay below.  Allows small-carb / treatment exclusions and pre-bolus detection."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift

@@ -8,7 +8,7 @@ struct MissedReadingEditor: View {
 
 
     var body: some View {
     var body: some View {
         Group {
         Group {
-            InfoBanner(text: "This warns you if the glucose monitor stops sending readings for too long..", alarmType: alarm.type)
+            InfoBanner(text: String(localized: "This warns you if the glucose monitor stops sending readings for too long.."), alarmType: alarm.type)
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 2
LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift

@@ -11,8 +11,7 @@ struct NotLoopingAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when no successful loop has occurred for the time "
-                    + "you set below.", alarmType: alarm.type
+                text: String(localized: "Alerts when no successful loop has occurred for the time you set below."), alarmType: alarm.type
             )
             )
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift

@@ -8,7 +8,7 @@ struct OverrideEndAlarmEditor: View {
 
 
     var body: some View {
     var body: some View {
         Group {
         Group {
-            InfoBanner(text: "Alerts when an override ends.", alarmType: alarm.type)
+            InfoBanner(text: String(localized: "Alerts when an override ends."), alarmType: alarm.type)
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift

@@ -9,7 +9,7 @@ struct OverrideStartAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when an override begins.",
+                text: String(localized: "Alerts when an override begins."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift

@@ -9,7 +9,7 @@ struct PhoneBatteryAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "This warns you when the phone's battery gets low, based on the percentage you choose.",
+                text: String(localized: "This warns you when the phone's battery gets low, based on the percentage you choose."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 3
LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift

@@ -9,9 +9,7 @@ struct PumpChangeAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when the pump / cannula is within the time "
-                    + "window you choose below (relative to the 3-day change "
-                    + "limit).",
+                text: String(localized: "Alerts when the pump / cannula is within the time window you choose below (relative to the 3-day change limit)."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift

@@ -9,7 +9,7 @@ struct PumpVolumeAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "This warns you when the insulin pump is running low on insulin.",
+                text: String(localized: "This warns you when the insulin pump is running low on insulin."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 2
LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift

@@ -9,8 +9,7 @@ struct RecBolusAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Alerts when the recommended bolus equals or exceeds the " +
-                    "threshold you set below.",
+                text: String(localized: "Alerts when the recommended bolus equals or exceeds the threshold you set below."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift

@@ -20,7 +20,7 @@ struct SensorAgeAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "Warn me before the sensor’s \(lifetimeDays)-day change-over.",
+                text: String(localized: "Warn me before the sensor’s \(lifetimeDays)-day change-over."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift

@@ -8,7 +8,7 @@ struct TempTargetEndAlarmEditor: View {
 
 
     var body: some View {
     var body: some View {
         Group {
         Group {
-            InfoBanner(text: "Alerts when a temp target ends.", alarmType: alarm.type)
+            InfoBanner(text: String(localized: "Alerts when a temp target ends."), alarmType: alarm.type)
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift

@@ -8,7 +8,7 @@ struct TempTargetStartAlarmEditor: View {
 
 
     var body: some View {
     var body: some View {
         Group {
         Group {
-            InfoBanner(text: "Alerts when a temp target starts.", alarmType: alarm.type)
+            InfoBanner(text: String(localized: "Alerts when a temp target starts."), alarmType: alarm.type)
 
 
             AlarmGeneralSection(alarm: $alarm)
             AlarmGeneralSection(alarm: $alarm)
 
 

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift

@@ -12,7 +12,7 @@ struct TemporaryAlarmEditor: View {
     var body: some View {
     var body: some View {
         Group {
         Group {
             InfoBanner(
             InfoBanner(
-                text: "This alert fires once when glucose crosses either of the limits you set below, and then disables itself.",
+                text: String(localized: "This alert fires once when glucose crosses either of the limits you set below, and then disables itself."),
                 alarmType: alarm.type
                 alarmType: alarm.type
             )
             )
 
 

+ 4 - 4
LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift

@@ -19,10 +19,10 @@ enum TimeUnit {
     /// A user-facing label
     /// A user-facing label
     var label: String {
     var label: String {
         switch self {
         switch self {
-        case .minute: return "min"
-        case .hour: return "hours"
-        case .day: return "days"
-        case .none: return "none"
+        case .minute: return String(localized: "min")
+        case .hour: return String(localized: "hours")
+        case .day: return String(localized: "days")
+        case .none: return String(localized: "none")
         }
         }
     }
     }
 }
 }

+ 2 - 2
LoopFollow/BackgroundRefresh/BT/BLEManager.swift

@@ -344,11 +344,11 @@ extension BLEManager {
         // If the heartbeat interval isn't a typical 60 or 300 seconds,
         // If the heartbeat interval isn't a typical 60 or 300 seconds,
         // we simply return a string indicating that the delay is "up to" the heartbeat interval.
         // we simply return a string indicating that the delay is "up to" the heartbeat interval.
         if heartBeatInterval != 60, heartBeatInterval != 300 {
         if heartBeatInterval != 60, heartBeatInterval != 300 {
-            return "up to \(Int(heartBeatInterval)) sec"
+            return String(localized: "up to \(Int(heartBeatInterval)) sec")
         }
         }
 
 
         let effectiveDelay = CycleHelper.computeDelay(sensorOffset: expectedOffset, heartbeatLast: heartbeatLast, heartbeatInterval: heartBeatInterval)
         let effectiveDelay = CycleHelper.computeDelay(sensorOffset: expectedOffset, heartbeatLast: heartbeatLast, heartbeatInterval: heartBeatInterval)
 
 
-        return "\(Int(effectiveDelay)) sec"
+        return String(localized: "\(Int(effectiveDelay)) sec")
     }
     }
 }
 }

+ 2 - 2
LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift

@@ -80,7 +80,7 @@ struct BackgroundRefreshSettingsView: View {
         if let storedDevice = bleManager.getSelectedDevice() {
         if let storedDevice = bleManager.getSelectedDevice() {
             Section(header: Text("Selected Device")) {
             Section(header: Text("Selected Device")) {
                 VStack(alignment: .leading, spacing: 4) {
                 VStack(alignment: .leading, spacing: 4) {
-                    Text(storedDevice.name ?? "Unknown Device")
+                    Text(storedDevice.name ?? String(localized: "Unknown Device"))
                         .font(.headline)
                         .font(.headline)
 
 
                     deviceConnectionStatus(for: storedDevice)
                     deviceConnectionStatus(for: storedDevice)
@@ -116,7 +116,7 @@ struct BackgroundRefreshSettingsView: View {
 
 
     private func formattedTimeString(from seconds: TimeInterval) -> String {
     private func formattedTimeString(from seconds: TimeInterval) -> String {
         if seconds < 60 {
         if seconds < 60 {
-            return "\(Int(seconds)) seconds"
+            return String(localized: "\(Int(seconds)) seconds")
         } else {
         } else {
             let minutes = Int(seconds / 60)
             let minutes = Int(seconds / 60)
             let seconds = Int(seconds.truncatingRemainder(dividingBy: 60))
             let seconds = Int(seconds.truncatingRemainder(dividingBy: 60))

+ 2 - 2
LoopFollow/Contact/ContactColorMode.swift

@@ -10,9 +10,9 @@ enum ContactColorMode: String, Codable, CaseIterable {
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
         case .staticColor:
         case .staticColor:
-            return "Static"
+            return String(localized: "Static")
         case .dynamic:
         case .dynamic:
-            return "Dynamic (BG Range)"
+            return String(localized: "Dynamic (BG Range)")
         }
         }
     }
     }
 
 

+ 5 - 5
LoopFollow/Helpers/AuthService.swift

@@ -22,22 +22,22 @@ public enum AuthService {
                                     completion: @escaping (AuthResult) -> Void)
                                     completion: @escaping (AuthResult) -> Void)
     {
     {
         let context = LAContext()
         let context = LAContext()
-        context.localizedFallbackTitle = "Enter Passcode"
+        context.localizedFallbackTitle = String(localized: "Enter Passcode")
         if reuseDuration > 0 {
         if reuseDuration > 0 {
             context.touchIDAuthenticationAllowableReuseDuration = reuseDuration
             context.touchIDAuthenticationAllowableReuseDuration = reuseDuration
         }
         }
 
 
         var error: NSError?
         var error: NSError?
         guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
         guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
-            var message = "Device authentication is not available. "
+            var message = String(localized: "Device authentication is not available. ")
 
 
             let biometryType = context.biometryType
             let biometryType = context.biometryType
             if biometryType == .none {
             if biometryType == .none {
-                message += "Please enable Face ID, Touch ID, or set up a device passcode in Settings."
+                message += String(localized: "Please enable Face ID, Touch ID, or set up a device passcode in Settings.")
             } else if biometryType == .faceID {
             } else if biometryType == .faceID {
-                message += "Face ID is not available. Please set up a device passcode in Settings."
+                message += String(localized: "Face ID is not available. Please set up a device passcode in Settings.")
             } else if biometryType == .touchID {
             } else if biometryType == .touchID {
-                message += "Touch ID is not available. Please set up a device passcode in Settings."
+                message += String(localized: "Touch ID is not available. Please set up a device passcode in Settings.")
             }
             }
 
 
             DispatchQueue.main.async {
             DispatchQueue.main.async {

+ 2 - 2
LoopFollow/Helpers/BuildDetails.swift

@@ -101,9 +101,9 @@ class BuildDetails {
     // Expiration header based on build type
     // Expiration header based on build type
     var expirationHeaderString: String {
     var expirationHeaderString: String {
         if isTestFlightBuild() {
         if isTestFlightBuild() {
-            return "TestFlight Expires"
+            return String(localized: "TestFlight Expires")
         } else {
         } else {
-            return "App Expires"
+            return String(localized: "App Expires")
         }
         }
     }
     }
 }
 }

+ 7 - 7
LoopFollow/Helpers/NightscoutUtils.swift

@@ -17,19 +17,19 @@ class NightscoutUtils {
         var errorDescription: String? {
         var errorDescription: String? {
             switch self {
             switch self {
             case .emptyAddress:
             case .emptyAddress:
-                return "The address is empty."
+                return String(localized: "The address is empty.")
             case .invalidURL:
             case .invalidURL:
-                return "The URL is invalid."
+                return String(localized: "The URL is invalid.")
             case .networkError:
             case .networkError:
-                return "A network error occurred."
+                return String(localized: "A network error occurred.")
             case .siteNotFound:
             case .siteNotFound:
-                return "The site was not found."
+                return String(localized: "The site was not found.")
             case .invalidToken:
             case .invalidToken:
-                return "The token is invalid."
+                return String(localized: "The token is invalid.")
             case .tokenRequired:
             case .tokenRequired:
-                return "A token is required."
+                return String(localized: "A token is required.")
             case .unknown:
             case .unknown:
-                return "An unknown error occurred."
+                return String(localized: "An unknown error occurred.")
             }
             }
         }
         }
     }
     }

+ 2 - 2
LoopFollow/Helpers/PredictionDisplayType.swift

@@ -7,8 +7,8 @@ enum PredictionDisplayType: String, CaseIterable, Codable {
 
 
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
-        case .cone: return "Cone"
-        case .lines: return "Lines"
+        case .cone: return String(localized: "Cone")
+        case .lines: return String(localized: "Lines")
         }
         }
     }
     }
 }
 }

+ 3 - 3
LoopFollow/Helpers/TextFieldWithToolBar.swift

@@ -177,20 +177,20 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
                         formatter.maximumFractionDigits = self.unit.preferredFractionDigits
                         formatter.maximumFractionDigits = self.unit.preferredFractionDigits
                         let step = pow(10.0, Double(-formatter.maximumFractionDigits))
                         let step = pow(10.0, Double(-formatter.maximumFractionDigits))
 
 
-                        var message = "Value outside of guardrails: \(text)\n"
+                        var message = String(localized: "Value outside of guardrails: \(text)\n")
 
 
                         if let minValue = self.parent.minValue {
                         if let minValue = self.parent.minValue {
                             let minVal = minValue.doubleValue(for: self.unit)
                             let minVal = minValue.doubleValue(for: self.unit)
                             let adjustedMin = ceil(minVal / step) * step
                             let adjustedMin = ceil(minVal / step) * step
                             let minQuantity = HKQuantity(unit: self.unit, doubleValue: adjustedMin)
                             let minQuantity = HKQuantity(unit: self.unit, doubleValue: adjustedMin)
-                            message += "Minimum: \(self.format(quantity: minQuantity, for: self.unit))\n"
+                            message += String(localized: "Minimum: \(self.format(quantity: minQuantity, for: self.unit))\n")
                         }
                         }
 
 
                         if let maxValue = self.parent.maxValue {
                         if let maxValue = self.parent.maxValue {
                             let maxVal = maxValue.doubleValue(for: self.unit)
                             let maxVal = maxValue.doubleValue(for: self.unit)
                             let adjustedMax = floor(maxVal / step) * step
                             let adjustedMax = floor(maxVal / step) * step
                             let maxQuantity = HKQuantity(unit: self.unit, doubleValue: adjustedMax)
                             let maxQuantity = HKQuantity(unit: self.unit, doubleValue: adjustedMax)
-                            message += "Maximum: \(self.format(quantity: maxQuantity, for: self.unit))"
+                            message += String(localized: "Maximum: \(self.format(quantity: maxQuantity, for: self.unit))")
                         }
                         }
 
 
                         self.onValidationError(message)
                         self.onValidationError(message)

+ 1 - 1
LoopFollow/Log/SearchBar.swift

@@ -29,7 +29,7 @@ struct SearchBar: UIViewRepresentable {
 
 
     func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
     func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
         let searchBar = UISearchBar(frame: .zero)
         let searchBar = UISearchBar(frame: .zero)
-        searchBar.placeholder = "Search Log"
+        searchBar.placeholder = String(localized: "Search Log")
         searchBar.delegate = context.coordinator
         searchBar.delegate = context.coordinator
         searchBar.autocapitalizationType = .none
         searchBar.autocapitalizationType = .none
         searchBar.searchBarStyle = .minimal
         searchBar.searchBarStyle = .minimal

Diferenças do arquivo suprimidas por serem muito extensas
+ 6900 - 5909
LoopFollow/Resources/Localizable.xcstrings


+ 8 - 8
LoopFollow/Settings/ImportExport/ExportableSettings.swift

@@ -333,12 +333,12 @@ struct RemoteSettingsExport: Codable {
 
 
         // If remote type is changing, warn user
         // If remote type is changing, warn user
         if currentRemoteType != .none && currentRemoteType != remoteType {
         if currentRemoteType != .none && currentRemoteType != remoteType {
-            message += "Remote type is changing from \(currentRemoteType.rawValue) to \(remoteType.rawValue). This may affect your remote commands.\n"
+            message += String(localized: "Remote type is changing from \(currentRemoteType.rawValue) to \(remoteType.rawValue). This may affect your remote commands.\n")
         }
         }
 
 
         // If user is changing, warn user
         // If user is changing, warn user
         if !currentUser.isEmpty && currentUser != user {
         if !currentUser.isEmpty && currentUser != user {
-            message += "Remote user is changing from '\(currentUser)' to '\(user)'. This may affect your remote commands.\n"
+            message += String(localized: "Remote user is changing from '\(currentUser)' to '\(user)'. This may affect your remote commands.\n")
         }
         }
 
 
         // For TRC and LoopAPNS, check if key details are changing
         // For TRC and LoopAPNS, check if key details are changing
@@ -347,11 +347,11 @@ struct RemoteSettingsExport: Codable {
             let currentApnsKey = storage.remoteApnsKey.value
             let currentApnsKey = storage.remoteApnsKey.value
 
 
             if !currentKeyId.isEmpty, currentKeyId != remoteKeyId {
             if !currentKeyId.isEmpty, currentKeyId != remoteKeyId {
-                message += "APNS Key ID is changing. This may affect your remote commands.\n"
+                message += String(localized: "APNS Key ID is changing. This may affect your remote commands.\n")
             }
             }
 
 
             if !currentApnsKey.isEmpty, currentApnsKey != remoteApnsKey {
             if !currentApnsKey.isEmpty, currentApnsKey != remoteApnsKey {
-                message += "APNS Key is changing. This may affect your remote commands.\n"
+                message += String(localized: "APNS Key is changing. This may affect your remote commands.\n")
             }
             }
         }
         }
 
 
@@ -359,7 +359,7 @@ struct RemoteSettingsExport: Codable {
         if remoteType == .trc {
         if remoteType == .trc {
             let currentSharedSecret = storage.sharedSecret.value
             let currentSharedSecret = storage.sharedSecret.value
             if !currentSharedSecret.isEmpty, currentSharedSecret != sharedSecret {
             if !currentSharedSecret.isEmpty, currentSharedSecret != sharedSecret {
-                message += "Shared secret is changing. This may affect your remote commands.\n"
+                message += String(localized: "Shared secret is changing. This may affect your remote commands.\n")
             }
             }
         }
         }
 
 
@@ -369,18 +369,18 @@ struct RemoteSettingsExport: Codable {
             let currentQrCodeURL = storage.loopAPNSQrCodeURL.value
             let currentQrCodeURL = storage.loopAPNSQrCodeURL.value
 
 
             if let teamId = teamId, let currentTeamId = currentTeamId, teamId != currentTeamId {
             if let teamId = teamId, let currentTeamId = currentTeamId, teamId != currentTeamId {
-                message += "Team ID is changing. This may affect your remote commands.\n"
+                message += String(localized: "Team ID is changing. This may affect your remote commands.\n")
             }
             }
 
 
             if !currentQrCodeURL.isEmpty, currentQrCodeURL != loopAPNSQrCodeURL {
             if !currentQrCodeURL.isEmpty, currentQrCodeURL != loopAPNSQrCodeURL {
-                message += "Loop APNS QR Code URL is changing. This may affect your remote commands.\n"
+                message += String(localized: "Loop APNS QR Code URL is changing. This may affect your remote commands.\n")
             }
             }
         }
         }
 
 
         // If both have tokens but they don't match, show warning
         // If both have tokens but they don't match, show warning
         let hasCurrentToken = !storage.token.value.isEmpty
         let hasCurrentToken = !storage.token.value.isEmpty
         if hasCurrentToken {
         if hasCurrentToken {
-            message += "Note: This import does not include Nightscout token settings. Your current Nightscout token will be preserved.\n"
+            message += String(localized: "Note: This import does not include Nightscout token settings. Your current Nightscout token will be preserved.\n")
         }
         }
 
 
         let isCompatible = !shouldPromptForURL && !shouldPromptForToken
         let isCompatible = !shouldPromptForURL && !shouldPromptForToken

+ 3 - 3
LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift

@@ -42,7 +42,7 @@ struct ImportExportSettingsView: View {
                         HStack {
                         HStack {
                             Image(systemName: exportType.icon)
                             Image(systemName: exportType.icon)
                                 .foregroundColor(.blue)
                                 .foregroundColor(.blue)
-                            Text("Export \(exportType.rawValue)")
+                            Text("Export \(String(localized: String.LocalizationValue(exportType.rawValue)))")
                             Spacer()
                             Spacer()
                             Image(systemName: exportType == .alarms ? "list.bullet" : "qrcode")
                             Image(systemName: exportType == .alarms ? "list.bullet" : "qrcode")
                                 .foregroundColor(.secondary)
                                 .foregroundColor(.secondary)
@@ -82,7 +82,7 @@ struct ImportExportSettingsView: View {
                         )
                         )
                         .padding()
                         .padding()
 
 
-                        Text("Scan this QR code with another LoopFollow app to import \(viewModel.exportType.importDescription)")
+                        Text("Scan this QR code with another LoopFollow app to import \(String(localized: String.LocalizationValue(viewModel.exportType.importDescription)))")
                             .font(.caption)
                             .font(.caption)
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
                             .multilineTextAlignment(.center)
                             .multilineTextAlignment(.center)
@@ -93,7 +93,7 @@ struct ImportExportSettingsView: View {
                             .padding()
                             .padding()
                     }
                     }
                 }
                 }
-                .navigationTitle("Export \(viewModel.exportType.rawValue)")
+                .navigationTitle("Export \(String(localized: String.LocalizationValue(viewModel.exportType.rawValue)))")
                 .navigationBarTitleDisplayMode(.inline)
                 .navigationBarTitleDisplayMode(.inline)
                 .navigationBarItems(trailing: Button("Close") {
                 .navigationBarItems(trailing: Button("Close") {
                     viewModel.isShowingQRCodeDisplay = false
                     viewModel.isShowingQRCodeDisplay = false

+ 12 - 12
LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift

@@ -71,7 +71,7 @@ class ImportExportSettingsViewModel: ObservableObject {
             case let .success(jsonString):
             case let .success(jsonString):
                 self.processImportedSettings(jsonString)
                 self.processImportedSettings(jsonString)
             case let .failure(error):
             case let .failure(error):
-                self.qrCodeErrorMessage = "Scanning failed: \(error.localizedDescription)"
+                self.qrCodeErrorMessage = String(localized: "Scanning failed: \(error.localizedDescription)")
             }
             }
             self.isShowingQRCodeScanner = false
             self.isShowingQRCodeScanner = false
         }
         }
@@ -82,7 +82,7 @@ class ImportExportSettingsViewModel: ObservableObject {
             LogManager.shared.log(category: .general, message: "Processing QR code data: \(LogRedactor.fingerprint(jsonString))")
             LogManager.shared.log(category: .general, message: "Processing QR code data: \(LogRedactor.fingerprint(jsonString))")
 
 
             guard let data = jsonString.data(using: .utf8) else {
             guard let data = jsonString.data(using: .utf8) else {
-                qrCodeErrorMessage = "Invalid QR code data"
+                qrCodeErrorMessage = String(localized: "Invalid QR code data")
                 return
                 return
             }
             }
 
 
@@ -100,7 +100,7 @@ class ImportExportSettingsViewModel: ObservableObject {
             // Use migration manager to handle version compatibility
             // Use migration manager to handle version compatibility
             guard let settings = SettingsMigrationManager.migrateSettings(data) else {
             guard let settings = SettingsMigrationManager.migrateSettings(data) else {
                 LogManager.shared.log(category: .general, message: "SettingsMigrationManager.migrateSettings returned nil")
                 LogManager.shared.log(category: .general, message: "SettingsMigrationManager.migrateSettings returned nil")
-                qrCodeErrorMessage = "Failed to decode or migrate settings from QR code"
+                qrCodeErrorMessage = String(localized: "Failed to decode or migrate settings from QR code")
                 return
                 return
             }
             }
 
 
@@ -120,7 +120,7 @@ class ImportExportSettingsViewModel: ObservableObject {
 
 
         } catch {
         } catch {
             let currentVersion = AppVersionManager().version()
             let currentVersion = AppVersionManager().version()
-            qrCodeErrorMessage = "Import failed. This might be due to a version change (current: \(currentVersion)). Please try exporting settings from the source device again."
+            qrCodeErrorMessage = String(localized: "Import failed. This might be due to a version change (current: \(currentVersion)). Please try exporting settings from the source device again.")
             LogManager.shared.log(category: .general, message: "QR code import failed: \(error.localizedDescription)")
             LogManager.shared.log(category: .general, message: "QR code import failed: \(error.localizedDescription)")
         }
         }
     }
     }
@@ -189,7 +189,7 @@ class ImportExportSettingsViewModel: ObservableObject {
         // Update the success message with what was imported
         // Update the success message with what was imported
         if !importedComponents.isEmpty {
         if !importedComponents.isEmpty {
             let componentsList = importedComponents.joined(separator: ", ")
             let componentsList = importedComponents.joined(separator: ", ")
-            qrCodeErrorMessage = "Successfully imported: \(componentsList)"
+            qrCodeErrorMessage = String(localized: "Successfully imported: \(componentsList)")
         }
         }
     }
     }
 
 
@@ -200,7 +200,7 @@ class ImportExportSettingsViewModel: ObservableObject {
         case .nightscout:
         case .nightscout:
             let nightscoutSettings = NightscoutSettingsExport.fromCurrentStorage()
             let nightscoutSettings = NightscoutSettingsExport.fromCurrentStorage()
             if !nightscoutSettings.hasValidSettings() {
             if !nightscoutSettings.hasValidSettings() {
-                qrCodeErrorMessage = "Please configure your Nightscout settings first (URL and Token)"
+                qrCodeErrorMessage = String(localized: "Please configure your Nightscout settings first (URL and Token)")
                 return nil
                 return nil
             }
             }
             settings = CombinedSettingsExport(
             settings = CombinedSettingsExport(
@@ -212,9 +212,9 @@ class ImportExportSettingsViewModel: ObservableObject {
             if !remoteSettings.hasValidSettings() {
             if !remoteSettings.hasValidSettings() {
                 let currentRemoteType = Storage.shared.remoteType.value
                 let currentRemoteType = Storage.shared.remoteType.value
                 if currentRemoteType == .none {
                 if currentRemoteType == .none {
-                    qrCodeErrorMessage = "Please configure your Remote settings first (select a remote type and configure required fields)"
+                    qrCodeErrorMessage = String(localized: "Please configure your Remote settings first (select a remote type and configure required fields)")
                 } else {
                 } else {
-                    qrCodeErrorMessage = "Please complete your Remote settings configuration (check required fields for \(currentRemoteType.rawValue))"
+                    qrCodeErrorMessage = String(localized: "Please complete your Remote settings configuration (check required fields for \(currentRemoteType.rawValue))")
                 }
                 }
                 return nil
                 return nil
             }
             }
@@ -226,7 +226,7 @@ class ImportExportSettingsViewModel: ObservableObject {
             let alarmSettings = AlarmSettingsExport.fromCurrentStorage()
             let alarmSettings = AlarmSettingsExport.fromCurrentStorage()
             LogManager.shared.log(category: .general, message: "Generating alarm export: \(alarmSettings.alarms.count) alarms")
             LogManager.shared.log(category: .general, message: "Generating alarm export: \(alarmSettings.alarms.count) alarms")
             if !alarmSettings.hasValidSettings() {
             if !alarmSettings.hasValidSettings() {
-                qrCodeErrorMessage = "Please configure your Alarm settings first"
+                qrCodeErrorMessage = String(localized: "Please configure your Alarm settings first")
                 return nil
                 return nil
             }
             }
             settings = CombinedSettingsExport(
             settings = CombinedSettingsExport(
@@ -236,7 +236,7 @@ class ImportExportSettingsViewModel: ObservableObject {
         case .dexcom:
         case .dexcom:
             let dexcomSettings = DexcomSettingsExport.fromCurrentStorage()
             let dexcomSettings = DexcomSettingsExport.fromCurrentStorage()
             if !dexcomSettings.hasValidSettings() {
             if !dexcomSettings.hasValidSettings() {
-                qrCodeErrorMessage = "Please configure your Dexcom Share settings first (username and password)"
+                qrCodeErrorMessage = String(localized: "Please configure your Dexcom Share settings first (username and password)")
                 return nil
                 return nil
             }
             }
             settings = CombinedSettingsExport(
             settings = CombinedSettingsExport(
@@ -317,7 +317,7 @@ class ImportExportSettingsViewModel: ObservableObject {
         } else {
         } else {
             LogManager.shared.log(category: .general, message: "No settings found, clearing import data")
             LogManager.shared.log(category: .general, message: "No settings found, clearing import data")
             // No settings found, show alert and clear any pending data
             // No settings found, show alert and clear any pending data
-            importNotFoundMessage = "The settings file exists but contains no valid settings to import."
+            importNotFoundMessage = String(localized: "The settings file exists but contains no valid settings to import.")
             showImportNotFoundAlert = true
             showImportNotFoundAlert = true
             showImportConfirmation = false
             showImportConfirmation = false
             importPreview = nil
             importPreview = nil
@@ -333,7 +333,7 @@ class ImportExportSettingsViewModel: ObservableObject {
         do {
         do {
             try applyImportedSettings(settings, source: pendingImportSource)
             try applyImportedSettings(settings, source: pendingImportSource)
         } catch {
         } catch {
-            qrCodeErrorMessage = "Import failed: \(error.localizedDescription)"
+            qrCodeErrorMessage = String(localized: "Import failed: \(error.localizedDescription)")
         }
         }
 
 
         // Reset confirmation state
         // Reset confirmation state

+ 1 - 1
LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift

@@ -85,7 +85,7 @@ class SettingsMigrationManager {
     }
     }
 
 
     static func getCompatibilityMessage(for version: String) -> String {
     static func getCompatibilityMessage(for version: String) -> String {
-        return "Settings from version \(version) may not be fully compatible with current version \(currentVersion). Some features may not work as expected."
+        return String(localized: "Settings from version \(version) may not be fully compatible with current version \(currentVersion). Some features may not work as expected.")
     }
     }
 
 
     // MARK: - Error Handling
     // MARK: - Error Handling

+ 1 - 1
LoopFollow/Snoozer/SnoozerViewModel.swift

@@ -7,7 +7,7 @@ import Foundation
 final class SnoozerViewModel: ObservableObject {
 final class SnoozerViewModel: ObservableObject {
     @Published var activeAlarm: Alarm?
     @Published var activeAlarm: Alarm?
     @Published var snoozeUnits: Int = 5
     @Published var snoozeUnits: Int = 5
-    @Published var timeUnitLabel: String = "minutes"
+    @Published var timeUnitLabel: String = String(localized: "minutes")
 
 
     private var cancellables = Set<AnyCancellable>()
     private var cancellables = Set<AnyCancellable>()
 
 

+ 1 - 1
LoopFollow/Storage/Observable.swift

@@ -20,7 +20,7 @@ class Observable {
     /// End of the active temp target as Unix epoch seconds; nil = no active temp target.
     /// End of the active temp target as Unix epoch seconds; nil = no active temp target.
     var tempTargetEndAt = ObservableValue<TimeInterval?>(default: nil)
     var tempTargetEndAt = ObservableValue<TimeInterval?>(default: nil)
 
 
-    var minAgoText = ObservableValue<String>(default: "?? min ago")
+    var minAgoText = ObservableValue<String>(default: String(localized: "?? min ago"))
     var bgText = ObservableValue<String>(default: "BG")
     var bgText = ObservableValue<String>(default: "BG")
     var bg = ObservableValue<Int?>(default: nil)
     var bg = ObservableValue<Int?>(default: nil)
     var bgStale = ObservableValue<Bool>(default: true)
     var bgStale = ObservableValue<Bool>(default: true)

+ 1 - 1
LoopFollow/Task/MinAgoTask.swift

@@ -39,7 +39,7 @@ extension MainViewController {
         }
         }
 
 
         let formattedDuration = formatter.string(from: secondsAgo) ?? ""
         let formattedDuration = formatter.string(from: secondsAgo) ?? ""
-        let minAgoDisplayText = formattedDuration + " min ago"
+        let minAgoDisplayText = formattedDuration + String(localized: " min ago")
 
 
         // Update UI only if the display text has changed
         // Update UI only if the display text has changed
         if minAgoDisplayText != Observable.shared.minAgoText.value {
         if minAgoDisplayText != Observable.shared.minAgoText.value {

+ 3 - 3
LoopFollow/Treatments/TreatmentsView.swift

@@ -1289,9 +1289,9 @@ class TreatmentsViewModel: ObservableObject {
                     ?? (entry["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
                     ?? (entry["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
                     ?? ""
                     ?? ""
 
 
-                let subtitleParts = ["Override", durationMinutes > 0 ? "\(Int(durationMinutes))m" : nil].compactMap { $0 }
+                let subtitleParts = [String(localized: "Override"), durationMinutes > 0 ? "\(Int(durationMinutes))m" : nil].compactMap { $0 }
                 let subtitle = subtitleParts.joined(separator: " • ")
                 let subtitle = subtitleParts.joined(separator: " • ")
-                let title = reason.isEmpty ? "Temporary Override" : reason
+                let title = reason.isEmpty ? String(localized: "Temporary Override") : reason
 
 
                 let treatment = Treatment(
                 let treatment = Treatment(
                     id: "\(nsId)-override",
                     id: "\(nsId)-override",
@@ -1327,7 +1327,7 @@ class TreatmentsViewModel: ObservableObject {
                 }
                 }
 
 
                 let reason = (entry["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
                 let reason = (entry["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
-                let subtitleParts = ["Temp Target", durationMinutes > 0 ? "\(Int(durationMinutes))m" : nil, reason?.isEmpty == false ? reason : nil].compactMap { $0 }
+                let subtitleParts = [String(localized: "Temp Target"), durationMinutes > 0 ? "\(Int(durationMinutes))m" : nil, reason?.isEmpty == false ? reason : nil].compactMap { $0 }
                 let subtitle = subtitleParts.joined(separator: " • ")
                 let subtitle = subtitleParts.joined(separator: " • ")
 
 
                 let treatment = Treatment(
                 let treatment = Treatment(

+ 5 - 5
LoopFollow/ViewControllers/MainViewController.swift

@@ -559,7 +559,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
     @objc func refresh() {
     @objc func refresh() {
         LogManager.shared.log(category: .general, message: "Refreshing")
         LogManager.shared.log(category: .general, message: "Refreshing")
 
 
-        Observable.shared.minAgoText.value = "Refreshing"
+        Observable.shared.minAgoText.value = String(localized: "Refreshing")
         scheduleAllTasks()
         scheduleAllTasks()
         NightscoutSocketManager.shared.connectIfNeeded()
         NightscoutSocketManager.shared.connectIfNeeded()
 
 
@@ -720,21 +720,21 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
             if isBlacklisted {
             if isBlacklisted {
                 let lastBlacklistShown = Storage.shared.lastBlacklistNotificationShown.value ?? Date.distantPast
                 let lastBlacklistShown = Storage.shared.lastBlacklistNotificationShown.value ?? Date.distantPast
                 if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours
                 if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours
-                    self.versionAlert(message: "The current version has a critical issue and should be updated as soon as possible.")
+                    self.versionAlert(message: String(localized: "The current version has a critical issue and should be updated as soon as possible."))
                     Storage.shared.lastBlacklistNotificationShown.value = now
                     Storage.shared.lastBlacklistNotificationShown.value = now
                     Storage.shared.lastVersionUpdateNotificationShown.value = now
                     Storage.shared.lastVersionUpdateNotificationShown.value = now
                 }
                 }
             } else if isNewer {
             } else if isNewer {
                 let lastVersionUpdateShown = Storage.shared.lastVersionUpdateNotificationShown.value ?? Date.distantPast
                 let lastVersionUpdateShown = Storage.shared.lastVersionUpdateNotificationShown.value ?? Date.distantPast
                 if now.timeIntervalSince(lastVersionUpdateShown) > 1_209_600 { // 2 weeks
                 if now.timeIntervalSince(lastVersionUpdateShown) > 1_209_600 { // 2 weeks
-                    self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.")
+                    self.versionAlert(message: String(localized: "A new version is available: \(latestVersion ?? String(localized: "Unknown")). It is recommended to update."))
                     Storage.shared.lastVersionUpdateNotificationShown.value = now
                     Storage.shared.lastVersionUpdateNotificationShown.value = now
                 }
                 }
             }
             }
         }
         }
     }
     }
 
 
-    func versionAlert(title: String = "Update Available", message: String) {
+    func versionAlert(title: String = String(localized: "Update Available"), message: String) {
         DispatchQueue.main.async {
         DispatchQueue.main.async {
             let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
             let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
             alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .default, handler: nil))
             alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .default, handler: nil))
@@ -914,7 +914,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
         var minAgo = ""
         var minAgo = ""
         if deltaTime > 9 {
         if deltaTime > 9 {
             // write old BG reading and continue pushing out end date to show last entry
             // write old BG reading and continue pushing out end date to show last entry
-            minAgo = String(Int(deltaTime)) + " min"
+            minAgo = String(Int(deltaTime)) + String(localized: " min")
             eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
             eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
         }
         }
         var basal = "~"
         var basal = "~"

+ 2 - 2
LoopFollow/ViewControllers/MoreMenuView.swift

@@ -263,8 +263,8 @@ struct MoreMenuView: View {
     private func shareLogs() {
     private func shareLogs() {
         let files = LogManager.shared.logFilesForTodayAndYesterday()
         let files = LogManager.shared.logFilesForTodayAndYesterday()
         guard !files.isEmpty else {
         guard !files.isEmpty else {
-            alertTitle = "No Logs Available"
-            alertMessage = "There are no logs to share."
+            alertTitle = String(localized: "No Logs Available")
+            alertMessage = String(localized: "There are no logs to share.")
             showAlert = true
             showAlert = true
             return
             return
         }
         }

+ 172 - 17
Scripts/gen_zh_catalog.py

@@ -1,12 +1,13 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-"""Generate LoopFollow/Resources/Localizable.xcstrings from a zh-Hans dict.
+"""Update LoopFollow/Resources/Localizable.xcstrings from a zh-Hans dict.
 Keys must match the exact source literal so SwiftUI auto-localizes.
 Keys must match the exact source literal so SwiftUI auto-localizes.
-Strings NOT in the dict are simply omitted -> app falls back to English.
+Existing catalog entries are preserved so Xcode-extracted source strings,
+plural variants, comments, and other languages are not discarded.
 """
 """
 import json, collections, os
 import json, collections, os
 
 
-APP = "/Users/luming/Desktop/loopfollow/loopfollow/LoopFollow"
+APP = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "LoopFollow")
 OUT = os.path.join(APP, "Resources", "Localizable.xcstrings")
 OUT = os.path.join(APP, "Resources", "Localizable.xcstrings")
 
 
 # English source -> 简体中文.  Terminology aligned with Trio-Diy glossary.
 # English source -> 简体中文.  Terminology aligned with Trio-Diy glossary.
@@ -539,7 +540,7 @@ add([
     ("Pump Battery Alert", "泵电量告警"),
     ("Pump Battery Alert", "泵电量告警"),
     ("Low Battery", "电量过低"),
     ("Low Battery", "电量过低"),
     ("Battery Drop", "电量骤降"),
     ("Battery Drop", "电量骤降"),
-    ("Rec. Bolus", "推荐剂量"),
+    ("Rec. Bolus", "推荐剂量"),
     ("Override Started", "临时配置已开始"),
     ("Override Started", "临时配置已开始"),
     ("Override Ended", "临时配置已结束"),
     ("Override Ended", "临时配置已结束"),
     ("Temp Target Started", "临时目标已开始"),
     ("Temp Target Started", "临时目标已开始"),
@@ -987,24 +988,178 @@ add([
     ("Unknown", "未知"),
     ("Unknown", "未知"),
 ])
 ])
 
 
+# ==== 三轮:首页 min ago 与缩写行名(用户点名) ====
+add([
+    (" min ago", " 分钟前"),
+    ("?? min ago", "?? 分钟前"),
+    ("Refreshing", "刷新中"),
+    (" min", " 分钟"),
+    ("IOB", "活性胰岛素"),
+    ("COB", "活性碳水"),
+    ("SAGE", "传感器"),
+    ("CAGE", "管路"),
+    ("IAGE", "胰岛素"),
+    ("TDD", "每日总量"),
+    ("ISF", "敏感系数"),
+    ("CR", "碳水系数"),
+])
+
+# ==== 三轮:版本提醒 / 身份验证 / 构建信息 ====
+add([
+    ("Update Available", "有可用更新"),
+    ("The current version has a critical issue and should be updated as soon as possible.", "当前版本存在严重问题,请尽快更新。"),
+    ("A new version is available: %@. It is recommended to update.", "新版本已发布:%@。建议尽快更新。"),
+    ("Enter Passcode", "输入设备密码"),
+    ("Device authentication is not available. ", "设备身份验证不可用。"),
+    ("Please enable Face ID, Touch ID, or set up a device passcode in Settings.", "请在「设置」中启用面容 ID、触控 ID,或设置设备密码。"),
+    ("Face ID is not available. Please set up a device passcode in Settings.", "面容 ID 不可用。请在「设置」中设置设备密码。"),
+    ("Touch ID is not available. Please set up a device passcode in Settings.", "触控 ID 不可用。请在「设置」中设置设备密码。"),
+    ("TestFlight Expires", "TestFlight 到期"),
+    ("App Expires", "应用到期"),
+    ("Search Log", "搜索日志"),
+    ("No Logs Available", "暂无日志"),
+    ("There are no logs to share.", "没有可分享的日志。"),
+])
+
+# ==== 三轮:时间单位 / 闹钟分组 / 默认闹钟名 ====
+add([
+    ("none", "无"),
+    ("Until", "直到"),
+    ("Insulin / Food", "胰岛素 / 食物"),
+    ("Device / System", "设备 / 系统"),
+    ("Override / Target", "临时配置 / 目标"),
+    ("Urgent Low", "紧急低血糖"),
+    ("Urgent High", "紧急高血糖"),
+    ("Fast Drop", "快速下降"),
+    ("Fast Rise", "快速上升"),
+    ("Missed Reading", "读数缺失"),
+    ("Sensor Change Reminder", "传感器更换提醒"),
+    ("Pump / Cannula Change", "泵/管路更换"),
+    ("Temporary BG Limit", "临时血糖限值"),
+    ("Pump Reservoir", "泵储药量"),
+    ("High IOB", "高活性胰岛素"),
+    ("High COB", "高活性碳水"),
+    ("Snoozed until %@", "已暂停至 %@"),
+    ("%lld sec", "%lld 秒"),
+    ("up to %lld sec", "最多 %lld 秒"),
+    ("%lld seconds", "%lld 秒"),
+    ("“Default Snooze” controls the default value for how long the alert stays quiet after you press Snooze. \"A snooze duration of 0 means the alarm is acknowledged (silenced), and will alert again next time the condition applies, without time limitation. \" Toggle “Snoozed” to mute this alarm right now.",
+     "「默认暂停时长」控制按下暂停后提醒静默多久的默认值。暂停时长为 0 表示仅确认(静音)该提醒,下次条件满足时仍会再次提醒,不受时间限制。打开「已暂停」可立即静音此闹钟。"),
+])
+
+# ==== 三轮:闹钟编辑器介绍段 ====
+add([
+    ("This warns you if your phone’s battery drops quickly, based on the percentage and time you set.", "当手机电量按你设定的幅度和时间快速下降时提醒你。"),
+    ("Alerts when Carbs-on-Board is at or above the amount you set below.", "当活性碳水达到或超过下方设定值时提醒。"),
+    ("Alerts when glucose readings rise rapidly. For example, three straight readings each climbing by at least the amount you set.  Optionally limit alerts to only fire above a certain BG.",
+     "当血糖读数快速上升时提醒。例如连续三次读数、每次上升都不低于你设定的幅度。还可限制仅在血糖高于某个值时才提醒。"),
+    ("Alerts when glucose stays above the limit you set below. Use Persistent if you want to ignore brief spikes.",
+     "当血糖持续高于下方设定的限值时提醒。若想忽略短暂冲高,请使用「持续时间」。"),
+    ("Sends a reminder before the looping-app build you’re following reaches its TestFlight or Xcode expiry date. Works with Trio 0.4 and later.",
+     "在你跟随的闭环应用构建到达 TestFlight 或 Xcode 到期日前发送提醒。适用于 Trio 0.4 及以上版本。"),
+    ("Alerts when glucose readings drop rapidly. For example, three straight readings each falling by at least the amount you set. Optionally limit alerts to only fire below a certain BG level.",
+     "当血糖读数快速下降时提醒。例如连续三次读数、每次下降都不低于你设定的幅度。还可限制仅在血糖低于某个值时才提醒。"),
+    ("Alerts when a future-dated carb entry's scheduled time arrives — a reminder to start eating. Use the max lookahead to ignore fat/protein entries that are typically scheduled further ahead.",
+     "当未来时间的碳水记录到点时提醒——提示该吃东西了。用最长预看时间可忽略通常安排得更靠后的脂肪/蛋白质条目。"),
+    ("Alerts when carbs are logged but no bolus is delivered within the delay below.  Allows small-carb / treatment exclusions and pre-bolus detection.",
+     "记录碳水后,若在下方延迟时间内未输注大剂量则提醒。支持排除少量碳水,并可识别餐前大剂量。"),
+    ("This warns you when the phone's battery gets low, based on the percentage you choose.", "当手机电量低于你选择的百分比时提醒你。"),
+    ("Alerts when insulin-on-board is high, or when several boluses in quick succession exceed the limits you set.",
+     "当活性胰岛素偏高,或短时间内多次大剂量超过你设定的限值时提醒。"),
+    ("Alerts when the pump / cannula is within the time window you choose below (relative to the 3-day change limit).",
+     "当泵/管路进入你在下方选择的时间窗口(相对 3 天更换期限)时提醒。"),
+    ("Alerts when the recommended bolus equals or exceeds the threshold you set below.", "当推荐大剂量达到或超过下方设定的阈值时提醒。"),
+    ("Alerts when an override begins.", "当临时配置开始时提醒。"),
+    ("Alerts when an override ends.", "当临时配置结束时提醒。"),
+    ("Alerts when a temp target starts.", "当临时目标开始时提醒。"),
+    ("Alerts when a temp target ends.", "当临时目标结束时提醒。"),
+    ("This alert fires once when glucose crosses either of the limits you set below, and then disables itself.",
+     "当血糖越过下方任一限值时提醒一次,然后自动停用。"),
+    ("This warns you when the insulin pump is running low on insulin.", "当胰岛素泵的药量不足时提醒你。"),
+    ("Alerts when no successful loop has occurred for the time you set below.", "当超过下方设定时间仍未成功闭环时提醒。"),
+    ("This warns you if the glucose is too low now or might be soon, based on the forecast.",
+     "当血糖现在过低、或根据预测即将过低时提醒你。"),
+    ("This warns you if the glucose monitor stops sending readings for too long..", "当血糖仪停止发送读数太久时提醒你。"),
+    ("Warn me before the sensor’s %lld-day change-over.", "在传感器 %lld 天更换期前提醒我。"),
+])
+
+# ==== 三轮:护栏 / Nightscout 校验 / 杂项 ====
+add([
+    ("Value outside of guardrails: %@\n", "数值超出安全护栏:%@\n"),
+    ("Minimum: %@\n", "最小值:%@\n"),
+    ("Maximum: %@", "最大值:%@"),
+    ("The address is empty.", "地址为空。"),
+    ("The URL is invalid.", "网址无效。"),
+    ("A network error occurred.", "发生网络错误。"),
+    ("The site was not found.", "未找到该站点。"),
+    ("The token is invalid.", "令牌无效。"),
+    ("A token is required.", "需要令牌。"),
+    ("An unknown error occurred.", "发生未知错误。"),
+    ("Cone", "锥形区间"),
+    ("Lines", "线条"),
+    ("Static", "固定"),
+    ("Dynamic (BG Range)", "动态(按血糖范围)"),
+    ("Unknown Device", "未知设备"),
+    ("Expected bg delay: %@", "预计血糖延迟:%@"),
+    ("Temporary Override", "临时配置"),
+    ("Add the contact named '%@' to your watch face to show the current BG value in real time. Make sure to give the app full access to Contacts when prompted.",
+     "把名为「%@」的联系人添加到表盘,即可实时显示当前血糖。弹出授权时请允许应用完整访问通讯录。"),
+    ("BG Update Delay (Sec): %lld", "血糖更新延迟(秒):%lld"),
+])
+
+# ==== 三轮:导入/导出 ====
+add([
+    ("Scanning failed: %@", "扫描失败:%@"),
+    ("Invalid QR code data", "二维码数据无效"),
+    ("Failed to decode or migrate settings from QR code", "无法解析或迁移二维码中的设置"),
+    ("Import failed. This might be due to a version change (current: %@). Please try exporting settings from the source device again.",
+     "导入失败。可能是版本变更所致(当前版本:%@)。请在源设备上重新导出设置后再试。"),
+    ("Successfully imported: %@", "导入成功:%@"),
+    ("Please configure your Nightscout settings first (URL and Token)", "请先配置你的 Nightscout 设置(网址和令牌)"),
+    ("Please complete your Remote settings configuration (check required fields for %@)", "请完善你的远程设置(检查 %@ 的必填项)"),
+    ("Please configure your Remote settings first (select a remote type and configure required fields)", "请先配置你的远程设置(选择远程类型并填写必填项)"),
+    ("Please configure your Alarm settings first", "请先配置你的闹钟设置"),
+    ("Please configure your Dexcom Share settings first (username and password)", "请先配置你的 Dexcom Share 设置(用户名和密码)"),
+    ("The settings file exists but contains no valid settings to import.", "设置文件存在,但不包含可导入的有效设置。"),
+    ("Import failed: %@", "导入失败:%@"),
+    ("Remote type is changing from %@ to %@. This may affect your remote commands.\n", "远程类型将从 %1$@ 变更为 %2$@。这可能影响你的远程指令。\n"),
+    ("Remote user is changing from '%@' to '%@'. This may affect your remote commands.\n", "远程用户将从「%1$@」变更为「%2$@」。这可能影响你的远程指令。\n"),
+    ("APNS Key ID is changing. This may affect your remote commands.\n", "APNS 密钥 ID 即将变更。这可能影响你的远程指令。\n"),
+    ("APNS Key is changing. This may affect your remote commands.\n", "APNS 密钥即将变更。这可能影响你的远程指令。\n"),
+    ("Shared secret is changing. This may affect your remote commands.\n", "共享密钥即将变更。这可能影响你的远程指令。\n"),
+    ("Team ID is changing. This may affect your remote commands.\n", "团队 ID 即将变更。这可能影响你的远程指令。\n"),
+    ("Loop APNS QR Code URL is changing. This may affect your remote commands.\n", "Loop APNS 二维码网址即将变更。这可能影响你的远程指令。\n"),
+    ("Note: This import does not include Nightscout token settings. Your current Nightscout token will be preserved.\n",
+     "注意:此次导入不包含 Nightscout 令牌设置。你当前的 Nightscout 令牌将被保留。\n"),
+    ("Settings from version %@ may not be fully compatible with current version %@. Some features may not work as expected.",
+     "来自版本 %1$@ 的设置可能与当前版本 %2$@ 不完全兼容,部分功能可能无法正常工作。"),
+    ("Nightscout Settings", "Nightscout 设置"),
+    ("Dexcom Share Settings", "Dexcom Share 设置"),
+    ("Scan this QR code with another LoopFollow app to import %@", "用另一台 LoopFollow 扫描此二维码以导入%@"),
+    ("Export %@", "导出%@"),
+])
+
 # ---------- 写出 ----------
 # ---------- 写出 ----------
-strings = collections.OrderedDict()
+if os.path.exists(OUT):
+    with open(OUT, "r", encoding="utf-8") as f:
+        catalog = json.load(f, object_pairs_hook=collections.OrderedDict)
+else:
+    catalog = collections.OrderedDict([
+        ("sourceLanguage", "en"),
+        ("strings", collections.OrderedDict()),
+        ("version", "1.0"),
+    ])
+
+strings = catalog.setdefault("strings", collections.OrderedDict())
 for en in sorted(T.keys()):
 for en in sorted(T.keys()):
     zh = T[en]
     zh = T[en]
-    strings[en] = {
-        "extractionState": "manual",
-        "localizations": {
-            "zh-Hans": {
-                "stringUnit": {"state": "translated", "value": zh}
-            }
-        },
+    entry = strings.setdefault(en, collections.OrderedDict())
+    entry.setdefault("extractionState", "manual")
+    localizations = entry.setdefault("localizations", collections.OrderedDict())
+    localizations["zh-Hans"] = {
+        "stringUnit": {"state": "translated", "value": zh}
     }
     }
 
 
-catalog = collections.OrderedDict()
-catalog["sourceLanguage"] = "en"
-catalog["strings"] = strings
-catalog["version"] = "1.0"
-
 os.makedirs(os.path.dirname(OUT), exist_ok=True)
 os.makedirs(os.path.dirname(OUT), exist_ok=True)
 with open(OUT, "w", encoding="utf-8") as f:
 with open(OUT, "w", encoding="utf-8") as f:
     json.dump(catalog, f, ensure_ascii=False, indent=2)
     json.dump(catalog, f, ensure_ascii=False, indent=2)