localization updates

This commit is contained in:
Stefan Lange-Hegermann
2025-10-21 10:43:51 +02:00
parent 28ad6dd10c
commit 4827ea4cdb
10 changed files with 846 additions and 63 deletions

View File

@@ -406,7 +406,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = RE4FXQ754N;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
@@ -441,7 +441,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = RE4FXQ754N;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;

View File

@@ -67,3 +67,48 @@
"system.icon.keywords.heat" = "heat, heater, furnace";
"system.icon.keywords.cold" = "cold, freeze, cool";
"system.icon.keywords.climate" = "climate, hvac, temperature";
"tab.components" = "Components";
"tab.batteries" = "Batteries";
"tab.chargers" = "Chargers";
"battery.bank.header.title" = "Battery Bank";
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacity";
"battery.bank.metric.energy" = "Energy";
"battery.bank.badge.voltage" = "Voltage";
"battery.bank.badge.capacity" = "Capacity";
"battery.bank.badge.energy" = "Energy";
"battery.bank.banner.voltage" = "Voltage mismatch detected";
"battery.bank.banner.capacity" = "Capacity mismatch detected";
"battery.bank.empty.title" = "No Batteries Yet";
"battery.bank.empty.subtitle" = "Tap the plus button to configure a battery for %@.";
"battery.bank.status.dismiss" = "Got it";
"battery.bank.status.single.battery" = "One battery";
"battery.bank.status.multiple.batteries" = "%d batteries";
"battery.bank.status.voltage.title" = "Voltage mismatch";
"battery.bank.status.voltage.message" = "%@ diverges from the bank baseline of %@. Mixing nominal voltages leads to uneven charging and can damage connected chargers or inverters.";
"battery.bank.status.capacity.title" = "Capacity mismatch";
"battery.bank.status.capacity.message" = "%@ uses a different capacity than the dominant bank size of %@. Mismatched capacities cause uneven discharge and premature wear.";
"battery.editor.title" = "Battery Setup";
"battery.editor.cancel" = "Cancel";
"battery.editor.save" = "Save";
"battery.editor.field.name" = "Name";
"battery.editor.placeholder.name" = "House Bank";
"battery.editor.field.chemistry" = "Chemistry";
"battery.editor.section.summary" = "Summary";
"battery.editor.slider.voltage" = "Nominal Voltage";
"battery.editor.slider.capacity" = "Capacity";
"battery.editor.alert.voltage.title" = "Edit Nominal Voltage";
"battery.editor.alert.voltage.placeholder" = "Voltage";
"battery.editor.alert.voltage.message" = "Enter voltage in volts (V)";
"battery.editor.alert.capacity.title" = "Edit Capacity";
"battery.editor.alert.capacity.placeholder" = "Capacity";
"battery.editor.alert.capacity.message" = "Enter capacity in amp-hours (Ah)";
"battery.editor.alert.cancel" = "Cancel";
"battery.editor.alert.save" = "Save";
"battery.editor.default_name" = "New Battery";
"chargers.title" = "Chargers for %@";
"chargers.subtitle" = "Charger components will be available soon.";

View File

@@ -6,6 +6,117 @@ struct BatteriesView: View {
let onEdit: (SavedBattery) -> Void
let onDelete: (IndexSet) -> Void
private enum BankStatus: Identifiable {
case voltage(target: Double, mismatchedCount: Int)
case capacity(target: Double, mismatchedCount: Int)
var id: String {
switch self {
case .voltage: return "voltage"
case .capacity: return "capacity"
}
}
var symbol: String {
switch self {
case .voltage: return "exclamationmark.triangle.fill"
case .capacity: return "exclamationmark.circle.fill"
}
}
var tint: Color {
switch self {
case .voltage: return .red
case .capacity: return .orange
}
}
var bannerText: String {
switch self {
case .voltage:
return String(
localized: "battery.bank.banner.voltage",
bundle: .main,
comment: "Short banner text describing a voltage mismatch"
)
case .capacity:
return String(
localized: "battery.bank.banner.capacity",
bundle: .main,
comment: "Short banner text describing a capacity mismatch"
)
}
}
}
private let voltageTolerance: Double = 0.05
private let capacityTolerance: Double = 0.5
@State private var activeStatus: BankStatus?
private var bankTitle: String {
String(
localized: "battery.bank.header.title",
bundle: .main,
comment: "Title for the battery bank summary section"
)
}
private var metricCountLabel: String {
String(
localized: "battery.bank.metric.count",
bundle: .main,
comment: "Label for number of batteries metric"
)
}
private var metricCapacityLabel: String {
String(
localized: "battery.bank.metric.capacity",
bundle: .main,
comment: "Label for total capacity metric"
)
}
private var metricEnergyLabel: String {
String(
localized: "battery.bank.metric.energy",
bundle: .main,
comment: "Label for total energy metric"
)
}
private var badgeVoltageLabel: String {
String(
localized: "battery.bank.badge.voltage",
bundle: .main,
comment: "Label for voltage badge"
)
}
private var badgeCapacityLabel: String {
String(
localized: "battery.bank.badge.capacity",
bundle: .main,
comment: "Label for capacity badge"
)
}
private var badgeEnergyLabel: String {
String(
localized: "battery.bank.badge.energy",
bundle: .main,
comment: "Label for energy badge"
)
}
private var emptyTitle: String {
String(
localized: "battery.bank.empty.title",
bundle: .main,
comment: "Title shown when no batteries are configured"
)
}
init(
system: ElectricalSystem,
batteries: [SavedBattery],
@@ -46,15 +157,31 @@ struct BatteriesView: View {
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.alert(item: $activeStatus) { status in
let detail = detailInfo(for: status)
return Alert(
title: Text(detail.title),
message: Text(detail.message),
dismissButton: .default(
Text(
NSLocalizedString(
"battery.bank.status.dismiss",
bundle: .main,
value: "Got it",
comment: "Dismiss button title for battery bank status alert"
)
)
)
)
}
}
private var summarySection: some View {
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
Text("Battery Bank")
.font(.headline)
.fontWeight(.semibold)
Text(bankTitle)
.font(.headline.weight(.semibold))
Spacer()
Text(system.name)
.font(.subheadline)
@@ -62,51 +189,60 @@ struct BatteriesView: View {
}
ViewThatFits(in: .horizontal) {
HStack(spacing: 12) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: "Batteries",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: "Capacity",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: "Energy",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
}
VStack(spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "battery.100",
label: "Batteries",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: "Capacity",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: "Energy",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
}
}
if let status = bankStatus {
Button {
activeStatus = status
} label: {
statusBanner(for: status)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.padding(.vertical, 10)
.background(Color(.systemGroupedBackground))
Divider()
@@ -149,17 +285,17 @@ struct BatteriesView: View {
HStack(spacing: 12) {
metricBadge(
label: "Voltage",
label: badgeVoltageLabel,
value: formattedValue(battery.nominalVoltage, unit: "V"),
tint: .orange
)
metricBadge(
label: "Capacity",
label: badgeCapacityLabel,
value: formattedValue(battery.capacityAmpHours, unit: "Ah"),
tint: .blue
)
metricBadge(
label: "Energy",
label: badgeEnergyLabel,
value: formattedValue(battery.energyWattHours, unit: "Wh"),
tint: .green
)
@@ -198,31 +334,19 @@ struct BatteriesView: View {
}
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
HStack(spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(tint.opacity(0.12))
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 16, weight: .semibold))
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(tint)
}
VStack(alignment: .leading, spacing: 2) {
Text(label.uppercased())
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Text(value)
.font(.subheadline.weight(.semibold))
.font(.body.weight(.semibold))
}
Text(label.uppercased())
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color(.secondarySystemBackground))
)
}
private func metricBadge(label: String, value: String, tint: Color) -> some View {
@@ -243,6 +367,24 @@ struct BatteriesView: View {
)
}
private func statusBanner(for status: BankStatus) -> some View {
HStack(spacing: 10) {
Image(systemName: status.symbol)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(status.tint)
Text(status.bannerText)
.font(.subheadline.weight(.semibold))
.foregroundStyle(status.tint)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(status.tint.opacity(0.12))
)
}
private func colorForName(_ colorName: String) -> Color {
switch colorName {
case "blue": return .blue
@@ -268,11 +410,11 @@ struct BatteriesView: View {
.font(.largeTitle)
.foregroundStyle(.secondary)
Text("No Batteries Yet")
Text(emptyTitle)
.font(.title3)
.fontWeight(.semibold)
Text("Tap the plus button to configure a battery for \(system.name).")
Text(emptySubtitle(for: system.name))
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
@@ -292,6 +434,149 @@ struct BatteriesView: View {
let numberString = Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
return "\(numberString) \(unit)"
}
private var dominantVoltage: Double? {
guard batteries.count > 1 else { return nil }
return dominantValue(
from: batteries.map { $0.nominalVoltage },
scale: 0.1
)
}
private var dominantCapacity: Double? {
guard batteries.count > 1 else { return nil }
return dominantValue(
from: batteries.map { $0.capacityAmpHours },
scale: 1.0
)
}
private func dominantValue(from values: [Double], scale: Double) -> Double? {
guard !values.isEmpty else { return nil }
var counts: [Double: Int] = [:]
var bestKey: Double?
var bestCount = 0
for value in values {
let key = (value / scale).rounded() * scale
let newCount = (counts[key] ?? 0) + 1
counts[key] = newCount
if newCount > bestCount {
bestCount = newCount
bestKey = key
}
}
return bestKey
}
private var bankStatus: BankStatus? {
guard batteries.count > 1 else { return nil }
if let targetVoltage = dominantVoltage {
let mismatched = batteries.filter { abs($0.nominalVoltage - targetVoltage) > voltageTolerance }
if !mismatched.isEmpty {
return .voltage(target: targetVoltage, mismatchedCount: mismatched.count)
}
}
if let targetCapacity = dominantCapacity {
let mismatched = batteries.filter { abs($0.capacityAmpHours - targetCapacity) > capacityTolerance }
if !mismatched.isEmpty {
return .capacity(target: targetCapacity, mismatchedCount: mismatched.count)
}
}
return nil
}
private func emptySubtitle(for systemName: String) -> String {
let format = NSLocalizedString(
"battery.bank.empty.subtitle",
tableName: nil,
bundle: .main,
value: "Tap the plus button to configure a battery for %@.",
comment: "Subtitle shown when no batteries are configured"
)
return String(format: format, systemName)
}
private func detailInfo(for status: BankStatus) -> (title: String, message: String) {
switch status {
case let .voltage(target, mismatchedCount):
let countText = mismatchedCount == 1
? NSLocalizedString(
"battery.bank.status.single.battery",
bundle: .main,
value: "One battery",
comment: "Singular form describing mismatched battery count"
)
: String(
format: NSLocalizedString(
"battery.bank.status.multiple.batteries",
bundle: .main,
value: "%d batteries",
comment: "Plural form describing mismatched battery count"
),
mismatchedCount
)
let expected = formattedValue(target, unit: "V")
let message = String(
format: NSLocalizedString(
"battery.bank.status.voltage.message",
bundle: .main,
value: "%@ diverges from the bank baseline of %@. Mixing nominal voltages leads to uneven charging and can damage connected chargers or inverters.",
comment: "Explanation for voltage mismatch in battery bank"
),
countText,
expected
)
return (
NSLocalizedString(
"battery.bank.status.voltage.title",
bundle: .main,
value: "Voltage mismatch",
comment: "Title for voltage mismatch alert"
),
message
)
case let .capacity(target, mismatchedCount):
let countText = mismatchedCount == 1
? NSLocalizedString(
"battery.bank.status.single.battery",
bundle: .main,
value: "One battery",
comment: "Singular form describing mismatched battery count"
)
: String(
format: NSLocalizedString(
"battery.bank.status.multiple.batteries",
bundle: .main,
value: "%d batteries",
comment: "Plural form describing mismatched battery count"
),
mismatchedCount
)
let expected = formattedValue(target, unit: "Ah")
let message = String(
format: NSLocalizedString(
"battery.bank.status.capacity.message",
bundle: .main,
value: "%@ uses a different capacity than the dominant bank size of %@. Mismatched capacities cause uneven discharge and premature wear.",
comment: "Explanation for capacity mismatch in battery bank"
),
countText,
expected
)
return (
NSLocalizedString(
"battery.bank.status.capacity.title",
bundle: .main,
value: "Capacity mismatch",
comment: "Title for capacity mismatch alert"
),
message
)
}
}
}
private enum BatteriesViewPreviewData {

View File

@@ -3,10 +3,93 @@ import SwiftUI
struct BatteryEditorView: View {
@Environment(\.dismiss) private var dismiss
@State private var configuration: BatteryConfiguration
@State private var editingField: EditingField?
let onSave: (BatteryConfiguration) -> Void
let onCancel: () -> Void
private enum EditingField {
case voltage
case capacity
}
private let voltageSnapValues: [Double] = [6, 12, 12.8, 24, 25.6, 36, 48, 51.2]
private let capacitySnapValues: [Double] = [10, 20, 50, 75, 100, 125, 150, 200, 300, 400, 600, 800, 1000]
private let voltageSnapTolerance: Double = 0.5
private let capacitySnapTolerance: Double = 10.0
private var nameFieldLabel: String {
String(
localized: "battery.editor.field.name",
bundle: .main,
comment: "Label for the battery name text field"
)
}
private var namePlaceholder: String {
String(
localized: "battery.editor.placeholder.name",
bundle: .main,
comment: "Placeholder example for the battery name field"
)
}
private var chemistryLabel: String {
String(
localized: "battery.editor.field.chemistry",
bundle: .main,
comment: "Label describing the chemistry menu"
)
}
private var summaryLabel: String {
String(
localized: "battery.editor.section.summary",
bundle: .main,
comment: "Label for the summary section in the editor"
)
}
private var sliderVoltageTitle: String {
String(
localized: "battery.editor.slider.voltage",
bundle: .main,
comment: "Title for the nominal voltage slider"
)
}
private var sliderCapacityTitle: String {
String(
localized: "battery.editor.slider.capacity",
bundle: .main,
comment: "Title for the capacity slider"
)
}
private var summaryVoltageLabel: String {
String(
localized: "battery.bank.badge.voltage",
bundle: .main,
comment: "Label used for voltage values"
)
}
private var summaryCapacityLabel: String {
String(
localized: "battery.bank.badge.capacity",
bundle: .main,
comment: "Label used for capacity values"
)
}
private var summaryEnergyLabel: String {
String(
localized: "battery.bank.badge.energy",
bundle: .main,
comment: "Label used for energy values"
)
}
init(configuration: BatteryConfiguration, onSave: @escaping (BatteryConfiguration) -> Void, onCancel: @escaping () -> Void) {
_configuration = State(initialValue: configuration)
self.onSave = onSave
@@ -59,15 +142,131 @@ struct BatteryEditorView: View {
.disabled(configuration.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
.alert(
NSLocalizedString(
"battery.editor.alert.voltage.title",
bundle: .main,
value: "Edit Nominal Voltage",
comment: "Title for the voltage edit alert"
),
isPresented: Binding(
get: { editingField == .voltage },
set: { if !$0 { editingField = nil } }
)
) {
TextField(
NSLocalizedString(
"battery.editor.alert.voltage.placeholder",
bundle: .main,
value: "Voltage",
comment: "Placeholder for voltage text field"
),
value: $configuration.nominalVoltage,
format: .number
)
.keyboardType(.decimalPad)
Button(
NSLocalizedString(
"battery.editor.alert.cancel",
bundle: .main,
value: "Cancel",
comment: "Cancel button title for edit alerts"
),
role: .cancel
) { editingField = nil }
Button(
NSLocalizedString(
"battery.editor.alert.save",
bundle: .main,
value: "Save",
comment: "Save button title for edit alerts"
)
) {
editingField = nil
let normalized = normalizedVoltage(for: configuration.nominalVoltage)
if abs(normalized - configuration.nominalVoltage) > 0.000001 {
configuration.nominalVoltage = normalized
}
}
} message: {
Text(
NSLocalizedString(
"battery.editor.alert.voltage.message",
bundle: .main,
value: "Enter voltage in volts (V)",
comment: "Message for the voltage edit alert"
)
)
}
.alert(
NSLocalizedString(
"battery.editor.alert.capacity.title",
bundle: .main,
value: "Edit Capacity",
comment: "Title for the capacity edit alert"
),
isPresented: Binding(
get: { editingField == .capacity },
set: { if !$0 { editingField = nil } }
)
) {
TextField(
NSLocalizedString(
"battery.editor.alert.capacity.placeholder",
bundle: .main,
value: "Capacity",
comment: "Placeholder for capacity text field"
),
value: $configuration.capacityAmpHours,
format: .number
)
.keyboardType(.decimalPad)
Button(
NSLocalizedString(
"battery.editor.alert.cancel",
bundle: .main,
value: "Cancel",
comment: "Cancel button title for edit alerts"
),
role: .cancel
) { editingField = nil }
Button(
NSLocalizedString(
"battery.editor.alert.save",
bundle: .main,
value: "Save",
comment: "Save button title for edit alerts"
)
) {
editingField = nil
let normalized = normalizedCapacity(for: configuration.capacityAmpHours)
if abs(normalized - configuration.capacityAmpHours) > 0.000001 {
configuration.capacityAmpHours = normalized
}
}
} message: {
Text(
NSLocalizedString(
"battery.editor.alert.capacity.message",
bundle: .main,
value: "Enter capacity in amp-hours (Ah)",
comment: "Message for the capacity edit alert"
)
)
}
}
private var headerCard: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("Name")
Text(nameFieldLabel)
.font(.caption)
.foregroundStyle(.secondary)
TextField("House Bank", text: $configuration.name)
TextField(namePlaceholder, text: $configuration.name)
.textInputAutocapitalization(.words)
.padding(.vertical, 10)
.padding(.horizontal, 12)
@@ -78,7 +277,7 @@ struct BatteryEditorView: View {
}
VStack(alignment: .leading, spacing: 8) {
Text("Chemistry")
Text(chemistryLabel)
.font(.caption)
.foregroundStyle(.secondary)
Menu {
@@ -113,23 +312,23 @@ struct BatteryEditorView: View {
}
VStack(alignment: .leading, spacing: 6) {
Text("Summary")
Text(summaryLabel)
.font(.caption)
.foregroundStyle(.secondary)
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
summaryBadge(
title: "Voltage",
title: summaryVoltageLabel,
value: formattedValue(configuration.nominalVoltage, unit: "V"),
symbol: "bolt"
)
summaryBadge(
title: "Capacity",
title: summaryCapacityLabel,
value: formattedValue(configuration.capacityAmpHours, unit: "Ah"),
symbol: "gauge.medium"
)
summaryBadge(
title: "Energy",
title: summaryEnergyLabel,
value: formattedValue(configuration.energyWattHours, unit: "Wh"),
symbol: "battery.100.bolt"
)
@@ -137,17 +336,17 @@ struct BatteryEditorView: View {
VStack(spacing: 12) {
summaryBadge(
title: "Voltage",
title: summaryVoltageLabel,
value: formattedValue(configuration.nominalVoltage, unit: "V"),
symbol: "bolt"
)
summaryBadge(
title: "Capacity",
title: summaryCapacityLabel,
value: formattedValue(configuration.capacityAmpHours, unit: "Ah"),
symbol: "gauge.medium"
)
summaryBadge(
title: "Energy",
title: summaryEnergyLabel,
value: formattedValue(configuration.energyWattHours, unit: "Wh"),
symbol: "battery.100.bolt"
)
@@ -165,19 +364,34 @@ struct BatteryEditorView: View {
private var slidersSection: some View {
VStack(spacing: 30) {
SliderSection(
title: "Nominal Voltage",
title: sliderVoltageTitle,
value: $configuration.nominalVoltage,
range: 6...60,
unit: "V",
snapValues: [6, 12, 12.8, 24, 25.6, 36, 48, 51.2]
tapAction: { editingField = .voltage },
snapValues: voltageSnapValues
)
.onChange(of: configuration.nominalVoltage) { _, newValue in
let normalized = normalizedVoltage(for: newValue)
if abs(normalized - newValue) > 0.000001 {
configuration.nominalVoltage = normalized
}
}
SliderSection(
title: "Capacity",
title: sliderCapacityTitle,
value: $configuration.capacityAmpHours,
range: 5...1000,
unit: "Ah",
snapValues: [10, 20, 50, 75, 100, 125, 150, 200, 300, 400, 600, 800, 1000]
tapAction: { editingField = .capacity },
snapValues: capacitySnapValues
)
.onChange(of: configuration.capacityAmpHours) { _, newValue in
let normalized = normalizedCapacity(for: newValue)
if abs(normalized - newValue) > 0.000001 {
configuration.capacityAmpHours = normalized
}
}
}
.padding()
.background(
@@ -186,6 +400,27 @@ struct BatteryEditorView: View {
)
}
private func normalizedVoltage(for value: Double) -> Double {
let rounded = (value * 10).rounded() / 10
if let snapped = nearestValue(to: rounded, in: voltageSnapValues, tolerance: voltageSnapTolerance) {
return snapped
}
return rounded
}
private func normalizedCapacity(for value: Double) -> Double {
let rounded = (value * 10).rounded() / 10
if let snapped = nearestValue(to: rounded, in: capacitySnapValues, tolerance: capacitySnapTolerance) {
return snapped
}
return rounded
}
private func nearestValue(to value: Double, in options: [Double], tolerance: Double) -> Double? {
guard let closest = options.min(by: { abs($0 - value) < abs($1 - value) }) else { return nil }
return abs(closest - value) <= tolerance ? closest : nil
}
private func summaryBadge(title: String, value: String, symbol: String) -> some View {
VStack(spacing: 4) {
Image(systemName: symbol)

View File

@@ -3,17 +3,34 @@ import SwiftUI
struct ChargersView: View {
let system: ElectricalSystem
private var titleText: String {
let format = NSLocalizedString(
"chargers.title",
bundle: .main,
comment: "Title describing chargers belonging to a system"
)
return String(format: format, system.name)
}
private var subtitleText: String {
String(
localized: "chargers.subtitle",
bundle: .main,
comment: "Subtitle shown while chargers tab is under construction"
)
}
var body: some View {
VStack(spacing: 16) {
Image(systemName: "bolt.fill")
.font(.largeTitle)
.foregroundStyle(.secondary)
Text("Chargers for \(system.name)")
Text(titleText)
.font(.title3)
.fontWeight(.semibold)
Text("Charger components will be available soon.")
Text(subtitleText)
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)

View File

@@ -50,7 +50,14 @@ struct LoadsView: View {
componentsTab
.tag(ComponentTab.components)
.tabItem {
Label("Components", systemImage: "square.stack.3d.up")
Label(
String(
localized: "tab.components",
bundle: .main,
comment: "Tab title for components list"
),
systemImage: "square.stack.3d.up"
)
}
BatteriesView(
system: system,
@@ -60,12 +67,26 @@ struct LoadsView: View {
)
.tag(ComponentTab.batteries)
.tabItem {
Label("Batteries", systemImage: "battery.100")
Label(
String(
localized: "tab.batteries",
bundle: .main,
comment: "Tab title for battery configurations"
),
systemImage: "battery.100"
)
}
ChargersView(system: system)
.tag(ComponentTab.chargers)
.tabItem {
Label("Chargers", systemImage: "bolt.fill")
Label(
String(
localized: "tab.chargers",
bundle: .main,
comment: "Tab title for chargers view"
),
systemImage: "bolt.fill"
)
}
}
}

View File

@@ -33,7 +33,7 @@
"slider.length.title" = "Kabellänge (%@)";
"slider.power.title" = "Leistung";
"slider.voltage.title" = "Spannung";
"system.list.no.components" = "Noch keine Komponenten";
"system.list.no.components" = "Noch keine Verbraucher";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metrisch (mm², m)";
"sample.system.rv.name" = "Abenteuer-Van";
@@ -78,14 +78,14 @@
"Create your first system" = "Erstelle dein erstes System";
"Give your setup a name so **Cable by VoltPlan** can organize loads, wiring, and recommendations in one place." = "Ob \"**Mein System**\", \"**Motoryacht LARGO**\" oder \"**Camper Luigi**\". Dein System braucht einen Namen.";
"Add your first component" = "Erstelle deine erste Komponente";
"Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations." = "Komponenten sind das Herz von Cable. Stelle sicher, dass du nie wieder ein zu dünnes Kabel oder die falsche Sicherung verwendest.";
"Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations." = "Verbraucher sind das Herz von Cable. Stelle sicher, dass du nie wieder ein zu dünnes Kabel oder die falsche Sicherung verwendest.";
"Create Component" = "Komponente erstellen";
"Browse Library" = "Bibliothek durchsuchen";
"Browse" = "Durchsuchen";
"Browse electrical components from VoltPlan" = "Elektrische Komponenten von VoltPlan durchstöbern";
"Browse electrical components from VoltPlan" = "Elektrische Verbraucher von VoltPlan durchstöbern";
"Component Library" = "Komponentenbibliothek";
"Details coming soon" = "Details folgen in Kürze";
"Components" = "Komponenten";
"Components" = "Verbraucher";
"FUSE" = "SICHERUNG";
"WIRE" = "KABEL";
"Current" = "Strom";
@@ -135,3 +135,48 @@
"Color" = "Farbe";
"VoltPlan Library" = "VoltPlan-Bibliothek";
"New Load" = "Neuer Verbraucher";
"tab.components" = "Verbraucher";
"tab.batteries" = "Batterien";
"tab.chargers" = "Ladegeräte";
"battery.bank.header.title" = "Batteriebank";
"battery.bank.metric.count" = "Batterien";
"battery.bank.metric.capacity" = "Kapazität";
"battery.bank.metric.energy" = "Energie";
"battery.bank.badge.voltage" = "Spannung";
"battery.bank.badge.capacity" = "Kapazität";
"battery.bank.badge.energy" = "Energie";
"battery.bank.banner.voltage" = "Spannungsabweichung erkannt";
"battery.bank.banner.capacity" = "Kapazitätsabweichung erkannt";
"battery.bank.empty.title" = "Noch keine Batterien";
"battery.bank.empty.subtitle" = "Tippe auf Plus, um eine Batterie für %@ zu konfigurieren.";
"battery.bank.status.dismiss" = "Verstanden";
"battery.bank.status.single.battery" = "Eine Batterie";
"battery.bank.status.multiple.batteries" = "%d Batterien";
"battery.bank.status.voltage.title" = "Spannungsabweichung";
"battery.bank.status.voltage.message" = "%@ weicht vom Bank-Basiswert %@ ab. Unterschiedliche Nennspannungen führen zu ungleichmäßigem Laden und können Ladegeräte oder Wechselrichter beschädigen.";
"battery.bank.status.capacity.title" = "Kapazitätsabweichung";
"battery.bank.status.capacity.message" = "%@ nutzt eine andere Kapazität als der dominante Bankwert %@. Unterschiedliche Kapazitäten verursachen ungleichmäßige Entladung und vorzeitige Alterung.";
"battery.editor.title" = "Batterie einrichten";
"battery.editor.cancel" = "Abbrechen";
"battery.editor.save" = "Speichern";
"battery.editor.field.name" = "Name";
"battery.editor.placeholder.name" = "Hausbank";
"battery.editor.field.chemistry" = "Chemie";
"battery.editor.section.summary" = "Übersicht";
"battery.editor.slider.voltage" = "Nennspannung";
"battery.editor.slider.capacity" = "Kapazität";
"battery.editor.alert.voltage.title" = "Nennspannung bearbeiten";
"battery.editor.alert.voltage.placeholder" = "Spannung";
"battery.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
"battery.editor.alert.capacity.title" = "Kapazität bearbeiten";
"battery.editor.alert.capacity.placeholder" = "Kapazität";
"battery.editor.alert.capacity.message" = "Kapazität in Amperestunden (Ah) eingeben";
"battery.editor.alert.cancel" = "Abbrechen";
"battery.editor.alert.save" = "Speichern";
"battery.editor.default_name" = "Neue Batterie";
"chargers.title" = "Ladegeräte für %@";
"chargers.subtitle" = "Ladegeräte-Komponenten sind bald verfügbar.";

View File

@@ -134,3 +134,48 @@
"Color" = "Color";
"VoltPlan Library" = "Biblioteca de VoltPlan";
"New Load" = "Carga nueva";
"tab.components" = "Componentes";
"tab.batteries" = "Baterías";
"tab.chargers" = "Cargadores";
"battery.bank.header.title" = "Banco de baterías";
"battery.bank.metric.count" = "Baterías";
"battery.bank.metric.capacity" = "Capacidad";
"battery.bank.metric.energy" = "Energía";
"battery.bank.badge.voltage" = "Voltaje";
"battery.bank.badge.capacity" = "Capacidad";
"battery.bank.badge.energy" = "Energía";
"battery.bank.banner.voltage" = "Se detectó un desajuste de voltaje";
"battery.bank.banner.capacity" = "Se detectó un desajuste de capacidad";
"battery.bank.empty.title" = "Sin baterías todavía";
"battery.bank.empty.subtitle" = "Toca el botón más para configurar una batería para %@.";
"battery.bank.status.dismiss" = "Entendido";
"battery.bank.status.single.battery" = "Una batería";
"battery.bank.status.multiple.batteries" = "%d baterías";
"battery.bank.status.voltage.title" = "Desajuste de voltaje";
"battery.bank.status.voltage.message" = "%@ se desvía del voltaje base del banco %@. Mezclar voltajes nominales provoca carga desigual y puede dañar cargadores o inversores conectados.";
"battery.bank.status.capacity.title" = "Desajuste de capacidad";
"battery.bank.status.capacity.message" = "%@ usa una capacidad distinta del valor dominante del banco %@. Las capacidades desiguales provocan descargas irregulares y desgaste prematuro.";
"battery.editor.title" = "Configuración de batería";
"battery.editor.cancel" = "Cancelar";
"battery.editor.save" = "Guardar";
"battery.editor.field.name" = "Nombre";
"battery.editor.placeholder.name" = "Banco principal";
"battery.editor.field.chemistry" = "Química";
"battery.editor.section.summary" = "Resumen";
"battery.editor.slider.voltage" = "Voltaje nominal";
"battery.editor.slider.capacity" = "Capacidad";
"battery.editor.alert.voltage.title" = "Editar voltaje nominal";
"battery.editor.alert.voltage.placeholder" = "Voltaje";
"battery.editor.alert.voltage.message" = "Introduce el voltaje en voltios (V)";
"battery.editor.alert.capacity.title" = "Editar capacidad";
"battery.editor.alert.capacity.placeholder" = "Capacidad";
"battery.editor.alert.capacity.message" = "Introduce la capacidad en amperios-hora (Ah)";
"battery.editor.alert.cancel" = "Cancelar";
"battery.editor.alert.save" = "Guardar";
"battery.editor.default_name" = "Nueva batería";
"chargers.title" = "Cargadores para %@";
"chargers.subtitle" = "Los componentes de carga estarán disponibles pronto.";

View File

@@ -134,3 +134,48 @@
"Color" = "Couleur";
"VoltPlan Library" = "Bibliothèque VoltPlan";
"New Load" = "Nouvelle charge";
"tab.components" = "Composants";
"tab.batteries" = "Batteries";
"tab.chargers" = "Chargeurs";
"battery.bank.header.title" = "Banque de batteries";
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacité";
"battery.bank.metric.energy" = "Énergie";
"battery.bank.badge.voltage" = "Tension";
"battery.bank.badge.capacity" = "Capacité";
"battery.bank.badge.energy" = "Énergie";
"battery.bank.banner.voltage" = "Écart de tension détecté";
"battery.bank.banner.capacity" = "Écart de capacité détecté";
"battery.bank.empty.title" = "Aucune batterie pour l'instant";
"battery.bank.empty.subtitle" = "Touchez le bouton plus pour configurer une batterie pour %@.";
"battery.bank.status.dismiss" = "Compris";
"battery.bank.status.single.battery" = "Une batterie";
"battery.bank.status.multiple.batteries" = "%d batteries";
"battery.bank.status.voltage.title" = "Écart de tension";
"battery.bank.status.voltage.message" = "%@ s'écarte de la valeur de référence %@ du banc. Mélanger des tensions nominales entraîne une charge inégale et peut endommager les chargeurs ou onduleurs connectés.";
"battery.bank.status.capacity.title" = "Écart de capacité";
"battery.bank.status.capacity.message" = "%@ utilise une capacité différente de la valeur dominante %@ du banc. Des capacités différentes provoquent des décharges inégales et une usure prématurée.";
"battery.editor.title" = "Configuration de la batterie";
"battery.editor.cancel" = "Annuler";
"battery.editor.save" = "Enregistrer";
"battery.editor.field.name" = "Nom";
"battery.editor.placeholder.name" = "Banque principale";
"battery.editor.field.chemistry" = "Chimie";
"battery.editor.section.summary" = "Résumé";
"battery.editor.slider.voltage" = "Tension nominale";
"battery.editor.slider.capacity" = "Capacité";
"battery.editor.alert.voltage.title" = "Modifier la tension nominale";
"battery.editor.alert.voltage.placeholder" = "Tension";
"battery.editor.alert.voltage.message" = "Saisissez la tension en volts (V)";
"battery.editor.alert.capacity.title" = "Modifier la capacité";
"battery.editor.alert.capacity.placeholder" = "Capacité";
"battery.editor.alert.capacity.message" = "Saisissez la capacité en ampères-heures (Ah)";
"battery.editor.alert.cancel" = "Annuler";
"battery.editor.alert.save" = "Enregistrer";
"battery.editor.default_name" = "Nouvelle batterie";
"chargers.title" = "Chargeurs pour %@";
"chargers.subtitle" = "Les chargeurs seront bientôt disponibles ici.";

View File

@@ -134,3 +134,48 @@
"Color" = "Kleur";
"VoltPlan Library" = "VoltPlan-bibliotheek";
"New Load" = "Nieuwe last";
"tab.components" = "Componenten";
"tab.batteries" = "Batterijen";
"tab.chargers" = "Laders";
"battery.bank.header.title" = "Accubank";
"battery.bank.metric.count" = "Batterijen";
"battery.bank.metric.capacity" = "Capaciteit";
"battery.bank.metric.energy" = "Energie";
"battery.bank.badge.voltage" = "Spanning";
"battery.bank.badge.capacity" = "Capaciteit";
"battery.bank.badge.energy" = "Energie";
"battery.bank.banner.voltage" = "Spanningsafwijking gedetecteerd";
"battery.bank.banner.capacity" = "Capaciteitsafwijking gedetecteerd";
"battery.bank.empty.title" = "Nog geen batterijen";
"battery.bank.empty.subtitle" = "Tik op de plusknop om een batterij voor %@ te configureren.";
"battery.bank.status.dismiss" = "Begrepen";
"battery.bank.status.single.battery" = "Eén batterij";
"battery.bank.status.multiple.batteries" = "%d batterijen";
"battery.bank.status.voltage.title" = "Spanningsafwijking";
"battery.bank.status.voltage.message" = "%@ wijkt af van de basiswaarde %@ van de bank. Verschillende nominale spanningen zorgen voor ongelijk laden en kunnen aangesloten laders of omvormers beschadigen.";
"battery.bank.status.capacity.title" = "Capaciteitsafwijking";
"battery.bank.status.capacity.message" = "%@ gebruikt een andere capaciteit dan de dominante bankwaarde %@. Verschillende capaciteiten zorgen voor ongelijk ontladen en vroegtijdige slijtage.";
"battery.editor.title" = "Batterij configureren";
"battery.editor.cancel" = "Annuleren";
"battery.editor.save" = "Opslaan";
"battery.editor.field.name" = "Naam";
"battery.editor.placeholder.name" = "Huishoudbank";
"battery.editor.field.chemistry" = "Chemie";
"battery.editor.section.summary" = "Overzicht";
"battery.editor.slider.voltage" = "Nominale spanning";
"battery.editor.slider.capacity" = "Capaciteit";
"battery.editor.alert.voltage.title" = "Nominale spanning bewerken";
"battery.editor.alert.voltage.placeholder" = "Spanning";
"battery.editor.alert.voltage.message" = "Voer de spanning in volt (V) in";
"battery.editor.alert.capacity.title" = "Capaciteit bewerken";
"battery.editor.alert.capacity.placeholder" = "Capaciteit";
"battery.editor.alert.capacity.message" = "Voer de capaciteit in ampère-uur (Ah) in";
"battery.editor.alert.cancel" = "Annuleren";
"battery.editor.alert.save" = "Opslaan";
"battery.editor.default_name" = "Nieuwe batterij";
"chargers.title" = "Laders voor %@";
"chargers.subtitle" = "Ladercomponenten zijn binnenkort beschikbaar.";