Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,9 @@
}
}
},
"%@ at %@" : {
"comment" : "VoiceOver label for a data point on a metrics chart. First %@ is the metric name, second %@ is the timestamp."
},
"%@ away" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -1998,6 +2001,12 @@
}
}
},
"%@ degrees" : {
"comment" : "VoiceOver value spoken as a temperature reading in degrees. %@ is the number."
},
"%@ devices" : {
"comment" : "VoiceOver value spoken as a device count on the PAX counter chart. %@ is the number."
},
"%@ documentation page" : {
"comment" : "Accessibility label and hint for the web view showing the documentation for a given page. The argument is the name of the page.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -2092,6 +2101,12 @@
}
}
},
"%@ milliamps" : {
"comment" : "VoiceOver value spoken as a current reading in milliamps. %@ is the number."
},
"%@ percent" : {
"comment" : "VoiceOver value spoken as a percentage reading on a chart. %@ is the number."
},
"%@ used on this device" : {
"comment" : "A footnote that shows the size of the offline map data on the user's device. The argument is the size of the offline map data in bytes.",
"isCommentAutoGenerated" : true,
Expand All @@ -2104,6 +2119,9 @@
}
}
},
"%@ volts" : {
"comment" : "VoiceOver value spoken as a voltage reading. %@ is the number."
},
"%@ will be freed on this device." : {
"comment" : "A message in a confirmation dialog that lets the user remove an offline map. The argument is the size of the map.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -10024,6 +10042,9 @@
}
}
},
"Airtime at %@" : {
"comment" : "VoiceOver label for an airtime point on the device metrics chart. %@ is the timestamp."
},
"Alert" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -11240,6 +11261,9 @@
}
}
},
"Altitude at %@" : {
"comment" : "VoiceOver label for an altitude point on the position altitude chart. %@ is the timestamp."
},
"Altitude is Mean Sea Level" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -14279,6 +14303,9 @@
}
}
},
"BLE devices at %@" : {
"comment" : "VoiceOver label for a BLE device-count point on the PAX counter chart. %@ is the timestamp."
},
"Back to firmware update" : {
"comment" : "VoiceOver label for the close button in the Chirpy game"
},
Expand Down Expand Up @@ -15121,6 +15148,9 @@
}
}
},
"Battery level at %@" : {
"comment" : "VoiceOver label for a battery-level point on the device metrics chart. %@ is the timestamp."
},
"Battery level, voltage, channel utilization, and airtime reported by the node." : {
"localizations" : {
"de" : {
Expand Down Expand Up @@ -19974,6 +20004,9 @@
}
}
},
"Channel utilization at %@" : {
"comment" : "VoiceOver label for a channel-utilization point on the device metrics chart. %@ is the timestamp."
},
"Channels" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -25789,6 +25822,9 @@
}
}
},
"Current at %@" : {
"comment" : "VoiceOver label for a current point on the power metrics chart. %@ is the timestamp."
},
"Current: %lld" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -28304,6 +28340,9 @@
}
}
},
"Detected" : {
"comment" : "VoiceOver value spoken for a detection-event bar on the detection sensor chart."
},
"Detection" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -28775,6 +28814,12 @@
}
}
},
"Detection event at %@" : {
"comment" : "VoiceOver label for a detection-event bar on the detection sensor chart. %@ is the timestamp."
},
"Detection events" : {
"comment" : "Legend label for the detection-events series on the detection sensor chart."
},
"Detection sensor" : {
"comment" : "VoiceOver label for the detection sensor message badge"
},
Expand Down Expand Up @@ -61163,6 +61208,9 @@
}
}
},
"No reading" : {
"comment" : "VoiceOver value spoken when a metric data point has no reading."
},
"No records found for %@" : {
"comment" : "A view that shows when a list is empty. The argument is the name of the entity being listed.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -94249,6 +94297,9 @@
}
}
},
"Temperature at %@" : {
"comment" : "VoiceOver label for a temperature point on the weather forecast chart. %@ is the time of day."
},
"Ten Minutes" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -99773,6 +99824,9 @@
}
}
},
"Total PAX at %@" : {
"comment" : "VoiceOver label for a total PAX point on the PAX counter chart. %@ is the timestamp."
},
"Total Unique Nodes" : {
"comment" : "A label for the total number of unique nodes found during a scan.",
"isCommentAutoGenerated" : true,
Expand Down Expand Up @@ -106990,6 +107044,9 @@
}
}
},
"Voltage at %@" : {
"comment" : "VoiceOver label for a voltage point on the power metrics chart. %@ is the timestamp."
},
"Volts %@" : {
"localizations" : {
"da" : {
Expand Down Expand Up @@ -108857,6 +108914,9 @@
}
}
},
"Wi-Fi devices at %@" : {
"comment" : "VoiceOver label for a Wi-Fi device-count point on the PAX counter chart. %@ is the timestamp."
},
"WiFi" : {
"localizations" : {
"da" : {
Expand Down
34 changes: 30 additions & 4 deletions Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class MetricsChartSeries: ObservableObject {
// Heading for space-constrained areas
let abbreviatedName: String

// Optional unit symbol (e.g. "°", "%", "hPa", "µg/m³") appended to VoiceOver values so the
// spoken reading includes its units. nil for unitless/index series.
let unit: String?

// Should this column appear in the chart
var visible: Bool

Expand All @@ -50,6 +54,7 @@ class MetricsChartSeries: ObservableObject {
keyPath: KeyPath<TelemetryEntity, Value>,
name: String,
abbreviatedName: String,
unit: String? = nil,
initialYAxisRange: ClosedRange<Float>? = nil,
minumumYAxisSpan: Float? = nil,
conversion: ((Value) -> Value)? = nil,
Expand All @@ -63,6 +68,7 @@ class MetricsChartSeries: ObservableObject {
self.id = id
self.name = name
self.abbreviatedName = abbreviatedName
self.unit = unit
self.initialYAxisRange = initialYAxisRange
self.minumumYAxisSpan = minumumYAxisSpan
self.visible = visible
Expand All @@ -72,11 +78,31 @@ class MetricsChartSeries: ObservableObject {
// This is a less elegant form of type erasure, but doesn't require a new Any-type
self.foregroundStyle = { range in foregroundStyle(range).map({ AnyShapeStyle($0) }) }
self.chartBodyClosure = { series, range, entity in
if let time = entity.time {
return AnyChartContent(
chartBody(series, range, time, entity[keyPath: keyPath]))
guard let time = entity.time else { return nil }
let content = chartBody(series, range, time, entity[keyPath: keyPath])
// Per-mark VoiceOver descriptors, applied centrally so every telemetry series
// (AirQuality + Environment charts) becomes navigable with consistent phrasing.
let timeText = time.formatted(date: .abbreviated, time: .shortened)
let label = String(
localized: "\(series.name.localized) at \(timeText)",
comment: "VoiceOver label for a data point on a metrics chart. First value is the metric name, second is the timestamp."
)
let valueText: String
if let value = series.valueClosure(entity) {
let number = Double(value).formatted(.number.precision(.fractionLength(1)))
if let unit = series.unit, !unit.isEmpty {
valueText = "\(number) \(unit)"
} else {
valueText = number
}
} else {
valueText = String(localized: "No reading", comment: "VoiceOver value spoken when a metric data point has no reading.")
}
return nil
return AnyChartContent(
content
.accessibilityLabel(label)
.accessibilityValue(valueText)
)
}
self.valueClosure = { te in
if let conversion {
Expand Down
5 changes: 5 additions & 0 deletions Meshtastic/Views/Helpers/Weather/NodeWeatherForecast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ struct NodeWeatherForecastView: View {
xEnd: .value("Hour", range.upperBound)
)
.opacity(0.5)
.accessibilityHidden(true)
.mask {
areaMarks(seriesKey: "Mask", value: range.lowerBound.timeIntervalSince1970)
}
Expand All @@ -68,6 +69,7 @@ struct NodeWeatherForecastView: View {
)
.foregroundStyle(.indigo.shadow(.drop(color: .white.opacity(0.25), radius: 0, x: 1)))
.cornerRadius(2)
.accessibilityHidden(true)
.annotation(position: .top, alignment: .bottom, spacing: 5) {
Image(systemName: "moon.circle.fill")
.imageScale(.large)
Expand All @@ -86,6 +88,7 @@ struct NodeWeatherForecastView: View {
)
.foregroundStyle(.indigo.shadow(.drop(color: .white.opacity(0.25), radius: 0, x: -1)))
.cornerRadius(2)
.accessibilityHidden(true)
.annotation(position: .top, alignment: .bottom, spacing: 5) {
Image(systemName: "sun.max.circle.fill")
.imageScale(.large)
Expand Down Expand Up @@ -122,6 +125,8 @@ struct NodeWeatherForecastView: View {
series: .value(seriesKey, value)
)
.interpolationMethod(.catmullRom)
.accessibilityLabel(String(localized: "Temperature at \(entry.date.formatted(date: .omitted, time: .shortened))", comment: "VoiceOver label for a temperature point on the weather forecast chart. %@ is the time of day."))
.accessibilityValue(String(localized: "\(entry.degrees.formatted(.number.precision(.fractionLength(0)))) degrees", comment: "VoiceOver value spoken as a temperature reading in degrees. %@ is the number."))
}
}

Expand Down
6 changes: 3 additions & 3 deletions Meshtastic/Views/Nodes/DetectionSensorLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ struct DetectionSensorLog: View {
y: .value("y", 1)
)
}
.accessibilityLabel("Bar Series")
.accessibilityValue("X: \(point.timestamp), Y: \(1)")
.accessibilityLabel(String(localized: "Detection event at \(point.timestamp.formatted(date: .abbreviated, time: .shortened))", comment: "VoiceOver label for a detection-event bar on the detection sensor chart. %@ is the timestamp."))
.accessibilityValue(String(localized: "Detected", comment: "VoiceOver value spoken for a detection-event bar on the detection sensor chart."))
.interpolationMethod(.cardinal)
.foregroundStyle(
.linearGradient(
Expand All @@ -55,7 +55,7 @@ struct DetectionSensorLog: View {
})
.chartXAxis(.automatic)
.chartForegroundStyleScale([
"Detection events": .green
"Detection events".localized: .green
])
.chartLegend(position: .automatic, alignment: .bottom)
}
Expand Down
21 changes: 12 additions & 9 deletions Meshtastic/Views/Nodes/DeviceMetricsLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ struct DeviceMetricsLog: View {
y: .value("y", batteryLevel)
)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(pointTime), Y: \(batteryLevel)")
.accessibilityLabel(String(localized: "Battery level at \(pointTime.formatted(date: .abbreviated, time: .shortened))", comment: "VoiceOver label for a battery-level point on the device metrics chart. %@ is the timestamp."))
.accessibilityValue(String(localized: "\(batteryLevel.formatted()) percent", comment: "VoiceOver value spoken as a percentage reading on a chart. %@ is the number."))
.foregroundStyle(batteryChartColor)
.interpolationMethod(.linear)
}
Expand All @@ -57,13 +57,14 @@ struct DeviceMetricsLog: View {
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(pointTime), Y: \(channelUtilization)")
.accessibilityLabel(String(localized: "Channel utilization at \(pointTime.formatted(date: .abbreviated, time: .shortened))", comment: "VoiceOver label for a channel-utilization point on the device metrics chart. %@ is the timestamp."))
.accessibilityValue(String(localized: "\(channelUtilization.formatted()) percent", comment: "VoiceOver value spoken as a percentage reading on a chart. %@ is the number."))
.foregroundStyle(channelUtilizationChartColor)
}
if let chartSelection {
RuleMark(x: .value("Second", chartSelection, unit: .second))
.foregroundStyle(.tertiary.opacity(0.5))
.accessibilityHidden(true)
}
if let airUtilTx = point.airUtilTx {
Plot {
Expand All @@ -73,18 +74,20 @@ struct DeviceMetricsLog: View {
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(pointTime), Y: \(airUtilTx)")
.accessibilityLabel(String(localized: "Airtime at \(pointTime.formatted(date: .abbreviated, time: .shortened))", comment: "VoiceOver label for an airtime point on the device metrics chart. %@ is the timestamp."))
.accessibilityValue(String(localized: "\(airUtilTx.formatted()) percent", comment: "VoiceOver value spoken as a percentage reading on a chart. %@ is the number."))
.foregroundStyle(airtimeChartColor)
}
}
}
RuleMark(y: .value("Network Status Orange", 25))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.orange)
.accessibilityHidden(true)
RuleMark(y: .value("Network Status Red", 50))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.red)
.accessibilityHidden(true)
}
.chartXAxis(content: {
AxisMarks(position: .top)
Expand All @@ -93,9 +96,9 @@ struct DeviceMetricsLog: View {
.chartXSelection(value: $chartSelection)
.chartYScale(domain: 0...100)
.chartForegroundStyleScale([
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
"Channel Utilization": channelUtilizationChartColor,
"Airtime": airtimeChartColor
(idiom == .phone ? "Battery".localized : "Battery Level".localized): batteryChartColor,
"Channel Utilization".localized: channelUtilizationChartColor,
"Airtime".localized: airtimeChartColor
])
.chartLegend(position: .automatic, alignment: .bottom)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ struct PositionAltitudeChart: View {
x: .value("Time", $0.time),
y: .value("Altitude", PlottableMeasurement(measurement: $0.altitude))
)
.accessibilityLabel($0.time.formatted(date: .abbreviated, time: .shortened))
.accessibilityValue("\($0.altitude)")
.accessibilityLabel(String(localized: "Altitude at \($0.time.formatted(date: .abbreviated, time: .shortened))", comment: "VoiceOver label for an altitude point on the position altitude chart. %@ is the timestamp."))
.accessibilityValue($0.altitude.converted(to: Locale.current.measurementSystem == .metric ? .meters : .feet).formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))
.lineStyle(StrokeStyle(lineWidth: lineWidth))
.interpolationMethod(.linear)
.symbol(Circle().strokeBorder(lineWidth: lineWidth))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ private func pmSeries(id: String, keyPath: KeyPath<TelemetryEntity, UInt32?>, na
keyPath: keyPath,
name: name,
abbreviatedName: abbreviatedName,
// PM concentrations are reported in micrograms per cubic meter.
unit: "µg/m³",
visible: visible,
foregroundStyle: { _ in
.linearGradient(colors: colors, startPoint: .bottom, endPoint: .top)
Expand Down
Loading
Loading