瀏覽代碼

Custom legends for full localization; add localize strings where missing

Deniz Cengiz 1 年之前
父節點
當前提交
6fd5ff47b3

+ 61 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -93422,6 +93422,16 @@
         }
       }
     },
+    "High (%@-%@)" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "High (%1$@-%2$@)"
+          }
+        }
+      }
+    },
     "High (>" : {
       "comment" : "Title High BG in statPanel",
       "extractionState" : "manual",
@@ -93730,6 +93740,9 @@
         }
       }
     },
+    "High Glucose" : {
+
+    },
     "High Glucose Alarm active" : {
       "comment" : "High Glucose Alarm active",
       "localizations" : {
@@ -99156,7 +99169,6 @@
     },
     "In Range" : {
       "comment" : "In Range",
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -109496,6 +109508,16 @@
         }
       }
     },
+    "Low (%@-%@)" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Low (%1$@-%2$@)"
+          }
+        }
+      }
+    },
     "Low (<" : {
       "comment" : "Title Low BG in statPanel",
       "extractionState" : "manual",
@@ -109912,6 +109934,9 @@
         }
       }
     },
+    "Low Glucose" : {
+
+    },
     "Low Glucose Alarm active" : {
       "comment" : "Low Glucose Alarm active",
       "localizations" : {
@@ -124194,6 +124219,16 @@
         }
       }
     },
+    "Normal (%@-%@)" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Normal (%1$@-%2$@)"
+          }
+        }
+      }
+    },
     "Normal Profile" : {
       "comment" : "Your normal Profile. Use a short string",
       "extractionState" : "manual",
@@ -149254,6 +149289,9 @@
         }
       }
     },
+    "Series" : {
+
+    },
     "Services" : {
       "localizations" : {
         "bg" : {
@@ -176542,6 +176580,16 @@
         }
       }
     },
+    "Tight (%@-%@)" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Tight (%1$@-%2$@)"
+          }
+        }
+      }
+    },
     "Time" : {
       "comment" : "Time basal profile",
       "localizations" : {
@@ -179361,6 +179409,9 @@
         }
       }
     },
+    "Tomorrow" : {
+
+    },
     "Top target" : {
       "comment" : "Upper temp target limit",
       "extractionState" : "manual",
@@ -189262,6 +189313,12 @@
         }
       }
     },
+    "Very High (>%@)" : {
+
+    },
+    "Very Low (<%@)" : {
+
+    },
     "Via Watch" : {
       "comment" : "Note added to carb entry when entered via watch",
       "localizations" : {
@@ -195063,6 +195120,9 @@
         }
       }
     },
+    "Yesterday" : {
+
+    },
     "You can connect Trio to seamlessly upload and manage your diabetes data on Tidepool." : {
       "localizations" : {
         "bg" : {

+ 26 - 4
Trio/Sources/Modules/Stat/View/StatChartUtils.swift

@@ -94,11 +94,11 @@ struct StatChartUtils {
 
         let formatDate: (Date) -> String = { date in
             if calendar.isDate(date, inSameDayAs: today) {
-                return "Today"
+                return String(localized: "Today")
             } else if calendar.isDate(date, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
-                return "Yesterday"
+                return String(localized: "Yesterday")
             } else if calendar.isDate(date, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
-                return "Tomorrow"
+                return String(localized: "Tomorrow")
             } else {
                 return date.formatted(.dateTime.day().month())
             }
@@ -124,7 +124,11 @@ struct StatChartUtils {
             Text(value)
         }
     }
-
+    
+    /// Computes the median value of an array of integers.
+    ///
+    /// - Parameter array: An array of integers.
+    /// - Returns: The median value as a `Double`. Returns `0` if the array is empty.
     static func medianCalculation(array: [Int]) -> Double {
         guard !array.isEmpty else { return 0 }
         let sorted = array.sorted()
@@ -136,6 +140,10 @@ struct StatChartUtils {
         return Double(sorted[length / 2])
     }
 
+    /// Computes the median value of an array of doubles.
+    ///
+    /// - Parameter array: An array of `Double` values.
+    /// - Returns: The median value. Returns `0` if the array is empty.
     static func medianCalculationDouble(array: [Double]) -> Double {
         guard !array.isEmpty else { return 0 }
         let sorted = array.sorted()
@@ -146,4 +154,18 @@ struct StatChartUtils {
         }
         return sorted[length / 2]
     }
+
+    /// Creates a legend item view for use in a chart legend.
+    ///
+    /// - Parameters:
+    ///   - label: The text label for the legend item.
+    ///   - color: The color associated with the legend item.
+    /// - Returns: A SwiftUI view displaying a colored symbol and a label.
+    @ViewBuilder
+    static func legendItem(label: String, color: Color) -> some View {
+        HStack(spacing: 4) {
+            Image(systemName: "circle.fill").foregroundStyle(color)
+            Text(label).foregroundStyle(Color.secondary)
+        }.font(.caption)
+    }
 }

+ 2 - 10
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift

@@ -32,7 +32,7 @@ struct GlucoseDistributionChart: View {
                 "200-220": .orange.opacity(0.7),
                 ">220": .orange.opacity(0.8)
             ])
-            .chartLegend(position: .bottom) {
+            .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
                     ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.7)),
                     (
@@ -59,11 +59,10 @@ struct GlucoseDistributionChart: View {
 
                 LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
                     ForEach(legendItems, id: \.0) { item in
-                        legendItem(label: item.0, color: item.1)
+                        StatChartUtils.legendItem(label: item.0, color: item.1)
                     }
                 }
             }
-
             .chartYAxis {
                 AxisMarks(position: .trailing) { value in
                     if let percentage = value.as(Double.self) {
@@ -91,11 +90,4 @@ struct GlucoseDistributionChart: View {
             .frame(height: 200)
         }
     }
-
-    @ViewBuilder func legendItem(label: String, color: Color) -> some View {
-        HStack(spacing: 4) {
-            Image(systemName: "circle.fill").foregroundStyle(color)
-            Text(label).foregroundStyle(Color.secondary)
-        }.font(.caption2)
-    }
 }

+ 32 - 29
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift

@@ -42,9 +42,6 @@ struct GlucosePercentileChart: View {
                 .font(.headline)
 
             Chart {
-                // TODO: ensure data is still correct
-                // TODO: ensure area marks and line mark take color of respective range
-
                 // Statistical view for longer periods
                 ForEach(hourlyStats, id: \.hour) { stats in
                     // 10-90 percentile area
@@ -54,7 +51,8 @@ struct GlucosePercentileChart: View {
                         yEnd: .value("90th Percentile", stats.percentile90),
                         series: .value("10-90", "10-90")
                     )
-                    .foregroundStyle(.blue.opacity(stats.median > 0 ? 0.2 : 0))
+                    .foregroundStyle(by: .value("Series", "10-90"))
+                    .opacity(stats.median > 0 ? 0.3 : 0)
 
                     // 25-75 percentile area
                     AreaMark(
@@ -63,7 +61,8 @@ struct GlucosePercentileChart: View {
                         yEnd: .value("75th Percentile", stats.percentile75),
                         series: .value("25-75", "25-75")
                     )
-                    .foregroundStyle(.blue.opacity(stats.median > 0 ? 0.4 : 0))
+                    .foregroundStyle(by: .value("Series", "25-75"))
+                    .opacity(stats.median > 0 ? 0.5 : 0)
 
                     // Median line
                     if stats.median > 0 {
@@ -73,18 +72,18 @@ struct GlucosePercentileChart: View {
                             series: .value("Median", "Median")
                         )
                         .lineStyle(StrokeStyle(lineWidth: 2))
-                        .foregroundStyle(.blue)
+                        .foregroundStyle(by: .value("Series", "Median"))
                     }
                 }
 
                 // High/Low limit lines
                 RuleMark(y: .value("High Limit", Double(highLimit)))
                     .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
-                    .foregroundStyle(.orange)
+                    .foregroundStyle(by: .value("Series", "High"))
 
                 RuleMark(y: .value("Low Limit", Double(lowLimit)))
                     .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
-                    .foregroundStyle(.red)
+                    .foregroundStyle(by: .value("Series", "Low"))
 
                 if let selectedStats, let selection {
                     RuleMark(x: .value("Selection", selection))
@@ -102,6 +101,30 @@ struct GlucosePercentileChart: View {
                         }
                 }
             }
+            .chartForegroundStyleScale([
+                "10-90": Color.blue.opacity(0.3),
+                "25-75": Color.blue.opacity(0.5),
+                "Median": Color.blue,
+                "High": Color.orange,
+                "Low": Color.red
+            ])
+            .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+                let legendItems: [(String, Color)] = [
+                    ("10-90%", Color.blue.opacity(0.3)),
+                    ("20-75%", Color.blue.opacity(0.5)),
+                    (String(localized: "Median"), Color.blue),
+                    (String(localized: "High Threshold"), Color.orange),
+                    (String(localized: "Low Threshold"), Color.red)
+                ]
+
+                let columns = [GridItem(.adaptive(minimum: 100), spacing: 4)]
+
+                LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                    ForEach(legendItems, id: \.0) { item in
+                        StatChartUtils.legendItem(label: item.0, color: item.1)
+                    }
+                }
+            }
             .chartYAxis {
                 AxisMarks(position: .trailing) { value in
                     if let glucose = value.as(Double.self) {
@@ -130,29 +153,9 @@ struct GlucosePercentileChart: View {
                 }
             }
             .chartXSelection(value: $selection.animation(.easeInOut))
-            .frame(height: 180)
-            legend
+            .frame(height: 200)
         }
     }
-
-    /// A view displaying the legend for the chart.
-    private var legend: some View {
-        HStack {
-            legendItem(color: .blue.opacity(0.2), text: "10-90%", icon: "rectangle.fill")
-            legendItem(color: .blue.opacity(0.4), text: "25-75%", icon: "rectangle.fill")
-            legendItem(color: .blue, text: "Median", icon: "rectangle.fill")
-        }
-    }
-
-    /// Creates a legend item with a given color and text.
-    private func legendItem(color: Color, text: String, icon: String) -> some View {
-        HStack(spacing: 8) {
-            Image(systemName: icon)
-                .foregroundStyle(color)
-            Text(text)
-                .foregroundStyle(.secondary)
-        }.font(.caption)
-    }
 }
 
 /// A popover view displaying detailed glucose statistics for a selected time.

+ 27 - 18
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift

@@ -204,14 +204,17 @@ struct GlucoseSectorChart: View {
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: highGlucoseValuesAsInt)
 
             return RangeDetail(
-                title: "High Glucose",
+                title: String(localized: "High Glucose"),
                 color: .orange,
                 items: [
-                    ("Very High (>\(formatValue(250)))", formatPercentage(Decimal(veryHigh) / total * 100)),
-                    ("High (\(formatValue(highLimit))-\(formatValue(250)))", formatPercentage(Decimal(high) / total * 100)),
-                    ("Average", formatValue(average)),
-                    ("Median", formatValue(median)),
-                    ("SD", formatSD(standardDeviation))
+                    (String(localized: "Very High (>\(formatValue(250)))"), formatPercentage(Decimal(veryHigh) / total * 100)),
+                    (
+                        String(localized: "High (\(formatValue(highLimit))-\(formatValue(250)))"),
+                        formatPercentage(Decimal(high) / total * 100)
+                    ),
+                    (String(localized: "Average"), formatValue(average)),
+                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
 
@@ -222,17 +225,20 @@ struct GlucoseSectorChart: View {
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: glucoseValuesAsInt)
 
             return RangeDetail(
-                title: "In Range",
+                title: String(localized: "In Range"),
                 color: .green,
                 items: [
                     (
-                        "Normal (\(formatValue(lowLimit))-\(formatValue(highLimit)))",
+                        String(localized: "Normal (\(formatValue(lowLimit))-\(formatValue(highLimit)))"),
                         formatPercentage(Decimal(glucoseValues.count) / total * 100)
                     ),
-                    ("Tight (\(formatValue(lowLimit))-\(formatValue(140)))", formatPercentage(Decimal(tight) / total * 100)),
-                    ("Average", formatValue(average)),
-                    ("Median", formatValue(median)),
-                    ("SD", formatSD(standardDeviation))
+                    (
+                        String(localized: "Tight (\(formatValue(lowLimit))-\(formatValue(140)))"),
+                        formatPercentage(Decimal(tight) / total * 100)
+                    ),
+                    (String(localized: "Average"), formatValue(average)),
+                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
 
@@ -245,14 +251,17 @@ struct GlucoseSectorChart: View {
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: lowGlucoseValuesAsInt)
 
             return RangeDetail(
-                title: "Low Glucose",
+                title: String(localized: "Low Glucose"),
                 color: .red,
                 items: [
-                    ("Low (\(formatValue(54))-\(formatValue(lowLimit)))", formatPercentage(Decimal(low) / total * 100)),
-                    ("Very Low (<\(formatValue(54)))", formatPercentage(Decimal(veryLow) / total * 100)),
-                    ("Average", formatValue(average)),
-                    ("Median", formatValue(median)),
-                    ("SD", formatSD(standardDeviation))
+                    (
+                        String(localized: "Low (\(formatValue(54))-\(formatValue(lowLimit)))"),
+                        formatPercentage(Decimal(low) / total * 100)
+                    ),
+                    (String(localized: "Very Low (<\(formatValue(54)))"), formatPercentage(Decimal(veryLow) / total * 100)),
+                    (String(localized: "Average"), formatValue(average)),
+                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
         }

+ 15 - 1
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -168,7 +168,21 @@ struct BolusStatsView: View {
             "Manual": Color.teal,
             "External": Color.purple
         ])
-        .chartLegend(position: .bottom, alignment: .leading, spacing: 12)
+        .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+            let legendItems: [(String, Color)] = [
+                (String(localized: "SMB"), Color.blue),
+                (String(localized: "Manual"), Color.teal),
+                (String(localized: "External"), Color.purple)
+            ]
+
+            let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]
+
+            LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                ForEach(legendItems, id: \.0) { item in
+                    StatChartUtils.legendItem(label: item.0, color: item.1)
+                }
+            }
+        }
         .chartYAxis {
             AxisMarks(position: .trailing) { value in
                 if let amount = value.as(Double.self) {

+ 19 - 5
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -172,12 +172,26 @@ struct MealStatsView: View {
                 }
             }
         }
-        .chartForegroundStyleScale(state.useFPUconversion ? [
+        .chartForegroundStyleScale([
             "Carbs": Color.orange,
-            "Fat": Color.green,
-            "Protein": Color.blue
-        ] : ["Carbs": Color.orange])
-        .chartLegend(position: .bottom, alignment: .leading, spacing: 12)
+            "Protein": Color.blue,
+            "Fat": Color.purple
+        ])
+        .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+            let legendItems: [(String, Color)] = state.useFPUconversion ? [
+                (String(localized: "Carbs"), Color.orange),
+                (String(localized: "Protein"), Color.blue),
+                (String(localized: "Fat"), Color.purple)
+            ] : [(String(localized: "Carbs"), Color.orange)]
+
+            let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]
+
+            LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                ForEach(legendItems, id: \.0) { item in
+                    StatChartUtils.legendItem(label: item.0, color: item.1)
+                }
+            }
+        }
         .chartYAxis {
             AxisMarks(position: .trailing) { value in
                 if let amount = value.as(Double.self) {